"
9 | }
10 | ],
11 | "main": "lib/module.js",
12 | "repository": "https://github.com/ax2inc/nuxt-modules",
13 | "homepage": "https://github.com/ax2inc/nuxt-modules/tree/master/packages/multisite#readme",
14 | "publishConfig": {
15 | "access": "public"
16 | },
17 | "files": [
18 | "lib"
19 | ],
20 | "dependencies": {
21 | "cookie": "^0.3.1",
22 | "deepmerge": "2.2.1",
23 | "js-cookie": "^2.2.0"
24 | },
25 | "devDependencies": {
26 | "codecov": "latest",
27 | "eslint": "5.13.0",
28 | "eslint-config-airbnb-base": "13.1.0",
29 | "eslint-loader": "2.1.2",
30 | "eslint-plugin-import": "2.16.0",
31 | "eslint-plugin-jest": "22.3.0",
32 | "eslint-plugin-vue": "5.2.1",
33 | "jest": "latest",
34 | "jsdom": "latest",
35 | "nuxt": "2.4.3",
36 | "request-promise-native": "1.0.5",
37 | "standard-version": "latest"
38 | },
39 | "gitHead": "db930bf38f5eec5696ad7978a68656436831fc59"
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AX2's Nuxt modules
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | This a monorepo containing Nuxt modules developed at Ax2.
26 | Have a look at the packages directory to see available modules.
27 |
28 |
29 |
30 | License
31 |
32 |
33 | MIT License
34 | Copyright (c) Ax2 Inc.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/packages/multisite/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [0.2.5](https://github.com/ax2inc/nuxt-modules/compare/@ax2/multisite-module@0.2.4...@ax2/multisite-module@0.2.5) (2019-03-15)
7 |
8 | **Note:** Version bump only for package @ax2/multisite-module
9 |
10 |
11 |
12 |
13 |
14 | ## [0.2.4](https://github.com/ax2inc/nuxt-modules/compare/@ax2/multisite-module@0.2.3...@ax2/multisite-module@0.2.4) (2019-03-15)
15 |
16 |
17 | ### Bug Fixes
18 |
19 | * Fix deepmerge call ([b612143](https://github.com/ax2inc/nuxt-modules/commit/b612143))
20 |
21 |
22 |
23 |
24 |
25 | ## [0.2.3](https://github.com/ax2inc/nuxt-modules/compare/@ax2/multisite-module@0.2.2...@ax2/multisite-module@0.2.3) (2019-03-15)
26 |
27 |
28 | ### Bug Fixes
29 |
30 | * Fix deepmerge import ([6eebdce](https://github.com/ax2inc/nuxt-modules/commit/6eebdce))
31 |
32 |
33 |
34 |
35 |
36 | ## [0.2.2](https://github.com/ax2inc/nuxt-modules/compare/@ax2/multisite-module@0.2.1...@ax2/multisite-module@0.2.2) (2019-03-15)
37 |
38 |
39 | ### Bug Fixes
40 |
41 | * **deps:** update dependency deepmerge to v3 ([7b7ab4a](https://github.com/ax2inc/nuxt-modules/commit/7b7ab4a))
42 |
43 |
44 |
45 |
46 |
47 | ## 0.2.1 (2018-10-18)
48 |
49 | **Note:** Version bump only for package @ax2/multisite-module
50 |
51 |
52 |
53 |
54 |
55 |
56 | # 0.2.0 (2018-09-27)
57 |
58 |
59 | ### Features
60 |
61 | * Add debug to force usage of query string when needed
62 |
63 |
64 | ## 0.1.1 (2018-09-15)
65 |
66 |
67 | ### Bug Fixes
68 |
69 | * Merge head properties using deepmerge
70 |
71 |
72 |
73 | # 0.1.0 (2018-09-08)
74 |
75 |
76 | ### Features
77 |
78 | * Initial commit
79 |
--------------------------------------------------------------------------------
/packages/dayjs/README.md:
--------------------------------------------------------------------------------
1 | # dayjs-module
2 | [](https://npmjs.com/package/@ax2/dayjs-module)
3 | [](https://npmjs.com/package/@ax2/dayjs-module)
4 | [](https://david-dm.org/ax2inc/dayjs-module)
5 | [](https://github.com/airbnb/javascript)
6 |
7 | > Day.js integration for Nuxt
8 |
9 | [📖 **Release Notes**](./CHANGELOG.md)
10 |
11 | ## Features
12 |
13 | Integrate [Day.js](https://github.com/iamkun/dayjs) with your Nuxt project.
14 |
15 | ## Setup
16 |
17 | - Install the module with your favorite package manager.
18 |
19 | ```sh
20 | yarn add @ax2/dayjs-module
21 | # Or npm i @ax2/dayjs-module
22 | ```
23 |
24 | - Add `dayjs-module` to `modules` section of `nuxt.config.js`.
25 |
26 | ```js
27 | // nuxt.config.js
28 |
29 | {
30 | modules: [
31 | '@ax2/dayjs-module',
32 | ],
33 | }
34 | ```
35 |
36 | - Configure the module as needed by adding a `dayjs` key to `nuxt.config.js`.
37 |
38 | ```js
39 | // nuxt.config.js
40 |
41 | {
42 | dayjs: {
43 | // Module options
44 | }
45 | }
46 | ```
47 |
48 | ## Usage
49 |
50 | At the moment, all the module does is inject Day.js into Vue instances so you can call it from anywhere in your app:
51 |
52 | ```vue
53 |
54 |
55 | {{ $dayjs('2018-09-18').format('dddd, MMMM DD, YYYY') }}
56 |
57 |
58 |
59 | ```
60 |
61 | ## Development
62 |
63 | - Clone this repository
64 | - Install dependencies using `yarn install` or `npm install`
65 | - Start development server using `npm run dev`
66 |
67 | ## License
68 |
69 | [MIT License](../../LICENSE)
70 |
71 | Copyright (c) Ax2 Inc.
72 |
--------------------------------------------------------------------------------
/packages/gpt-ads/test/module.test.js:
--------------------------------------------------------------------------------
1 | const { Nuxt, Builder } = require('nuxt');
2 | const jsdom = require('jsdom');
3 | const request = require('request-promise-native');
4 |
5 | const PORT = 3001;
6 | const config = require('./fixture/nuxt.config');
7 |
8 | const { JSDOM } = jsdom;
9 | const getDom = html => (new JSDOM(html)).window.document;
10 |
11 | const url = path => `http://localhost:${PORT}${path}`;
12 | const get = path => request(url(path));
13 |
14 | const GPT_LIB_SCRIPT_ID = 'google-publisher-tag-lib-script';
15 | const GPT_INIT_SCRIPT_ID = 'google-publisher-tag-init-script';
16 |
17 | describe('basic', () => {
18 | let nuxt;
19 |
20 | beforeAll(async () => {
21 | nuxt = new Nuxt(config);
22 | await new Builder(nuxt).build();
23 | await nuxt.listen(PORT);
24 | }, 60000);
25 |
26 | afterAll(async () => {
27 | await nuxt.close();
28 | });
29 |
30 | test('Render', async () => {
31 | const html = await get('/');
32 | expect(html).toContain('Works!');
33 | });
34 |
35 | test('Injects Google Publisher Tag lib', async () => {
36 | const html = await get('/');
37 | const dom = getDom(html);
38 | const node = dom.querySelector(`head #${GPT_LIB_SCRIPT_ID}`);
39 | expect(node.src).toBe('https://www.googletagservices.com/tag/js/gpt.js');
40 | });
41 |
42 | test('Injects Google Publisher Tag init script', async () => {
43 | const html = await get('/');
44 | const dom = getDom(html);
45 | const node = dom.querySelector(`head #${GPT_INIT_SCRIPT_ID}`);
46 | expect(node).not.toBeNull();
47 | });
48 |
49 | test('Properly handles individualRefresh option', async () => {
50 | const html = await get('/');
51 | expect(html).toContain('googletag.pubads().disableInitialLoad();');
52 | });
53 |
54 | test('Renders a div for the ad', async () => {
55 | const html = await get('/');
56 | const dom = getDom(html);
57 | const node = dom.querySelector('#ad-container > div');
58 | expect(node).not.toBeNull();
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/packages/gpt-ads/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | # [0.6.0](https://github.com/ax2inc/nuxt-modules/compare/@ax2/gpt-ads-module@0.5.3...@ax2/gpt-ads-module@0.6.0) (2019-03-15)
7 |
8 |
9 | ### Features
10 |
11 | * Toggle class on ads slots when they become empty ([a3863f1](https://github.com/ax2inc/nuxt-modules/commit/a3863f1))
12 |
13 |
14 |
15 |
16 |
17 | ## [0.5.3](https://github.com/ax2inc/nuxt-modules/compare/@ax2/gpt-ads-module@0.5.2...@ax2/gpt-ads-module@0.5.3) (2018-11-01)
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * Test window.googletag rather than googletag, prevents an error in SPA mode ([38699d7](https://github.com/ax2inc/nuxt-modules/commit/38699d7))
23 |
24 |
25 |
26 |
27 |
28 | ## 0.5.2 (2018-10-18)
29 |
30 | **Note:** Version bump only for package @ax2/gpt-ads-module
31 |
32 |
33 |
34 |
35 |
36 |
37 | ## 0.5.1 (2018-10-11)
38 |
39 |
40 | ### Bug Fixes
41 |
42 | * Prevent error with size mappings that have a single size definition
43 |
44 |
45 | # 0.5.0 (2018-10-11)
46 |
47 |
48 | ### Features
49 |
50 | * Add ghostMode option
51 |
52 |
53 | ## 0.4.1 (2018-10-05)
54 |
55 |
56 | ### Bug Fixes
57 |
58 | * Rely on self-built sizes mapping
59 |
60 |
61 | # 0.4.0 (2018-10-04)
62 |
63 |
64 | ### Bug Fixes
65 |
66 | * Prevent TypeError if mapping were to be undefined
67 |
68 |
69 | ### Features
70 |
71 | * Add collapseEmptyDivs option & collapseEmptyDiv prop
72 |
73 |
74 | # 0.3.0 (2018-10-04)
75 |
76 |
77 | ### Features
78 |
79 | * Add responsive option and isResponsive prop
80 |
81 |
82 | # 0.2.0 (2018-09-27)
83 |
84 |
85 | ### Features
86 |
87 | * Add individualRefresh option
88 |
89 |
90 | ## 0.1.1 (2018-09-26)
91 |
92 |
93 | ### Bug Fixes
94 |
95 | * Slot size should be treated as a list
96 |
97 |
98 | # 0.1.0 (2018-09-11)
99 |
100 |
101 | ### Features
102 |
103 | * Initial commit
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## macOS.gitignore
2 | ## https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
3 |
4 | # General
5 | .DS_Store
6 | .AppleDouble
7 | .LSOverride
8 |
9 | # Icon must end with two \r
10 | Icon
11 |
12 |
13 | # Thumbnails
14 | ._*
15 |
16 | # Files that might appear in the root of a volume
17 | .DocumentRevisions-V100
18 | .fseventsd
19 | .Spotlight-V100
20 | .TemporaryItems
21 | .Trashes
22 | .VolumeIcon.icns
23 | .com.apple.timemachine.donotpresent
24 |
25 | # Directories potentially created on remote AFP share
26 | .AppleDB
27 | .AppleDesktop
28 | Network Trash Folder
29 | Temporary Items
30 | .apdisk
31 |
32 |
33 | ## Node.gitignore
34 | ## https://github.com/github/gitignore/blob/master/Node.gitignore
35 |
36 | # Logs
37 | logs
38 | *.log
39 | npm-debug.log*
40 | yarn-debug.log*
41 | yarn-error.log*
42 |
43 | # Runtime data
44 | pids
45 | *.pid
46 | *.seed
47 | *.pid.lock
48 |
49 | # Directory for instrumented libs generated by jscoverage/JSCover
50 | lib-cov
51 |
52 | # Coverage directory used by tools like istanbul
53 | coverage
54 |
55 | # nyc test coverage
56 | .nyc_output
57 |
58 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
59 | .grunt
60 |
61 | # Bower dependency directory (https://bower.io/)
62 | bower_components
63 |
64 | # node-waf configuration
65 | .lock-wscript
66 |
67 | # Compiled binary addons (https://nodejs.org/api/addons.html)
68 | build/Release
69 |
70 | # Dependency directories
71 | node_modules/
72 | jspm_packages/
73 |
74 | # TypeScript v1 declaration files
75 | typings/
76 |
77 | # Optional npm cache directory
78 | .npm
79 |
80 | # Optional eslint cache
81 | .eslintcache
82 |
83 | # Optional REPL history
84 | .node_repl_history
85 |
86 | # Output of 'npm pack'
87 | *.tgz
88 |
89 | # Yarn Integrity file
90 | .yarn-integrity
91 |
92 | # dotenv environment variables file
93 | .env
94 |
95 | # parcel-bundler cache (https://parceljs.org/)
96 | .cache
97 |
98 | # next.js build output
99 | .next
100 |
101 | # nuxt.js build output
102 | .nuxt
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # Serverless directories
108 | .serverless
--------------------------------------------------------------------------------
/packages/gpt-ads/lib/templates/plugin.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default async function (ctx, inject) {
4 |
5 | const { app } = ctx;
6 |
7 | // Module options
8 | const debug = <%= options.debug || false %>;
9 | const individualRefresh = <%= options.individualRefresh || false %>;
10 | const collapseEmptyDivs = <%= options.collapseEmptyDivs || false %>;
11 | const networkCode = '<%= options.networkCode %>';
12 | const GPT_LIB_SCRIPT_ID = '<%= options.GPT_LIB_SCRIPT_ID %>';
13 | const GPT_INIT_SCRIPT_ID = '<%= options.GPT_INIT_SCRIPT_ID %>';
14 |
15 | // Instance options
16 | const gptAdsOptions = {
17 | networkCode,
18 | individualRefresh,
19 | slots: [],
20 | };
21 |
22 | const injectScript = (script) => {
23 | const scriptIndex = ctx.app.head.script.findIndex(s => s.id === script.id);
24 | if (scriptIndex !== -1) {
25 | ctx.app.head.script[scriptIndex] = script;
26 | } else {
27 | ctx.app.head.script.push(script);
28 | }
29 | };
30 |
31 | // Inject GPT lib
32 | const gptLibScript = {
33 | id: GPT_LIB_SCRIPT_ID,
34 | src: 'https://www.googletagservices.com/tag/js/gpt.js',
35 | async: true,
36 | };
37 | injectScript(gptLibScript);
38 |
39 | // Inject GPT init script
40 | let gptInitScriptHtml = 'var googletag = googletag || {};googletag.cmd = googletag.cmd || [];';
41 | if (debug) {
42 | gptInitScriptHtml += 'googletag.cmd.push(function(){googletag.openConsole();});';
43 | }
44 | // Disable initial load
45 | const gptDisableInitialLoad = individualRefresh ? 'googletag.pubads().disableInitialLoad();' : '';
46 | // Collapse empty div
47 | const gptCollapseEmptyDivs = collapseEmptyDivs ? 'googletag.pubads().collapseEmptyDivs();' : '';
48 |
49 | gptInitScriptHtml += `
50 | googletag.cmd.push(function(){
51 | googletag.pubads().enableSingleRequest();
52 | ${gptDisableInitialLoad}
53 | ${gptCollapseEmptyDivs}
54 | googletag.enableServices();
55 | });
56 | `;
57 | const gptInitScript = {
58 | id: GPT_INIT_SCRIPT_ID,
59 | innerHTML: gptInitScriptHtml,
60 | };
61 | injectScript(gptInitScript);
62 |
63 | const component = require('./component.js');
64 | Vue.component('<%= options.componentName %>', component.default || component);
65 |
66 | inject('gptAds', gptAdsOptions);
67 | }
68 |
--------------------------------------------------------------------------------
/packages/lozad/README.md:
--------------------------------------------------------------------------------
1 | # lozad-module
2 | [](https://npmjs.com/package/@ax2/lozad-module)
3 | [](https://npmjs.com/package/@ax2/lozad-module)
4 | [](https://david-dm.org/ax2inc/lozad-module)
5 | [](https://github.com/airbnb/javascript)
6 |
7 | > Lozad.js integration for Nuxt
8 |
9 | [📖 **Release Notes**](./CHANGELOG.md)
10 |
11 | ## Features
12 |
13 | Integrate [Lozad.js](https://github.com/ApoorvSaxena/lozad.js) with your Nuxt project.
14 |
15 | ## Setup
16 |
17 | - Install the module with your favorite package manager.
18 |
19 | ```sh
20 | yarn add @ax2/lozad-module
21 | # Or npm i @ax2/lozad-module
22 | ```
23 |
24 | - Add `lozad-module` to `modules` section of `nuxt.config.js`.
25 |
26 | ```js
27 | // nuxt.config.js
28 |
29 | {
30 | modules: [
31 | '@ax2/lozad-module',
32 | ],
33 | }
34 | ```
35 |
36 | - Configure the module as needed by adding a `lozad` key to `nuxt.config.js`.
37 |
38 | ```js
39 | // nuxt.config.js
40 |
41 | {
42 | lozad: {
43 | // Module options
44 | }
45 | }
46 | ```
47 |
48 | ## Options
49 |
50 | ### selector
51 |
52 | - **Type**: `String`
53 | - **Default**: `'.lozad'`
54 |
55 | Selector which lozad uses to find elements to be lazy-loaded.
56 |
57 | ### observer
58 |
59 | - **Type**: `Object`
60 | - **Default**: `{}`
61 |
62 | IntersectionObserver options, see [lozad options](https://apoorv.pro/lozad.js/#usage).
63 |
64 | ### polyfill
65 |
66 | - **Type**: `Boolean`
67 | - **Default**: `false`
68 |
69 | Set to `true` to enable [IntersectionObserver](https://caniuse.com/#feat=intersectionobserver) polyfill.
70 |
71 | ## Usage
72 |
73 | To enable lazy-loading, you must trigger lozad's `observe()` method in the `mounted()` hook of your pages/components that include lazy-loadable content.
74 |
75 | ```vue
76 |
77 |
78 |
![]()
79 |
80 |
81 |
82 |
89 |
90 | ```
91 |
92 |
93 | ## Development
94 |
95 | - Clone this repository
96 | - Install dependencies using `yarn install` or `npm install`
97 | - Start development server using `npm run dev`
98 |
99 | ## License
100 |
101 | [MIT License](../../LICENSE)
102 |
103 | Copyright (c) Ax2 Inc.
104 |
--------------------------------------------------------------------------------
/packages/multisite/test/module.test.js:
--------------------------------------------------------------------------------
1 | const { Nuxt, Builder } = require('nuxt');
2 | const jsdom = require('jsdom');
3 | const request = require('request-promise-native');
4 |
5 | const PORT = 3003;
6 | const config = require('./fixture/nuxt.config');
7 |
8 | const { JSDOM } = jsdom;
9 | const getDom = html => (new JSDOM(html)).window.document;
10 |
11 | const url = path => `http://localhost:${PORT}${path}`;
12 | const get = path => request(url(path));
13 | const getComputedCssVars = (cssVars) => {
14 | const style = Object.keys(cssVars).map(key => `${key}:${cssVars[key]}`);
15 | return `:root{${style.join(';')}}`;
16 | };
17 |
18 | describe('basic', () => {
19 | let nuxt;
20 |
21 | beforeAll(async () => {
22 | nuxt = new Nuxt(config);
23 | await new Builder(nuxt).build();
24 | await nuxt.listen(PORT);
25 | }, 60000);
26 |
27 | afterAll(async () => {
28 | await nuxt.close();
29 | });
30 |
31 | test('Render', async () => {
32 | const html = await get('/');
33 | expect(html).toContain('Works!');
34 | });
35 |
36 | /**
37 | * localhost
38 | */
39 | describe(`localhost - ${config.multisite.sites[0].id}`, () => {
40 | const siteUrl = path => `http://localhost:${PORT}${path}`;
41 | const siteGet = path => request(siteUrl(path));
42 |
43 | test('Enables proper site', async () => {
44 | const html = await siteGet('/');
45 | expect(html).toContain('My awesome site');
46 | });
47 |
48 | test('Sets proper css vars', async () => {
49 | const html = await siteGet('/');
50 | const dom = getDom(html);
51 | const node = dom.querySelector('#multisite-css-vars');
52 | expect(node.textContent).toBe(getComputedCssVars(config.multisite.sites[0].cssVars));
53 | });
54 |
55 | test('Uses proper favicon', async () => {
56 | const html = await request(`http://localhost:${PORT}/`);
57 | const dom = getDom(html);
58 | const node = dom.querySelector('link[rel="icon"]');
59 | expect(node.href).toBe(config.multisite.sites[0].head.link[0].href);
60 | });
61 | });
62 |
63 | /**
64 | * 127.0.0.1
65 | */
66 | describe(`127.0.0.1 - ${config.multisite.sites[1].id}`, () => {
67 | const siteUrl = path => `http://127.0.0.1:${PORT}${path}`;
68 | const siteGet = path => request(siteUrl(path));
69 |
70 | test('Enables proper site', async () => {
71 | const html = await siteGet('/');
72 | expect(html).toContain('Another cool site');
73 | });
74 |
75 | test('Sets proper css vars', async () => {
76 | const html = await siteGet('/');
77 | const dom = getDom(html);
78 | const node = dom.querySelector('#multisite-css-vars');
79 | expect(node.textContent).toBe(getComputedCssVars(config.multisite.sites[1].cssVars));
80 | });
81 |
82 | test('Uses proper favicon', async () => {
83 | const html = await siteGet('/');
84 | const dom = getDom(html);
85 | const node = dom.querySelector('link[rel="icon"]');
86 | expect(node.href).toBe(config.multisite.sites[1].head.link[0].href);
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/packages/multisite/lib/templates/plugin.js:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import cookie from 'cookie';
3 | import deepmerge from 'deepmerge';
4 |
5 | export default (ctx, inject) => {
6 | const {
7 | req,
8 | res,
9 | query,
10 | isDev,
11 | } = ctx;
12 |
13 | // Module options
14 | const sites = <%= JSON.stringify(options.sites) %>;
15 | const debug = <%= options.debug %>;
16 | const QUERY_SITE_ID_KEY = '<%= options.QUERY_SITE_ID_KEY %>';
17 | const COOKIE_SITE_KEY = '<%= options.COOKIE_SITE_KEY %>';
18 | const CSS_VARS_STYLE_ID = '<%= options.CSS_VARS_STYLE_ID %>';
19 | const defaultSite = sites.find(site => site.isDefault);
20 |
21 | // Instance options
22 | const multisiteOptions = {
23 | site: null,
24 | };
25 |
26 | // Guess current site
27 | let currentSiteId = null;
28 | // In development, get current site from cookies or query string
29 | if (debug || isDev) {
30 | currentSiteId = query[QUERY_SITE_ID_KEY];
31 |
32 | // If no site ID found in query string, attempt to retrieve it from cookies
33 | if (!currentSiteId) {
34 | if (process.server) {
35 | const cookies = cookie.parse(req.headers.cookie || '');
36 | currentSiteId = cookies[COOKIE_SITE_KEY] || null;
37 | } else {
38 | currentSiteId = Cookies.get(COOKIE_SITE_KEY) || null;
39 | }
40 | }
41 |
42 | // Set cookie
43 | if (currentSiteId && process.server) {
44 | res.setHeader('Set-Cookie', cookie.serialize(COOKIE_SITE_KEY, String(currentSiteId), {
45 | httpOnly: false,
46 | path: '/',
47 | maxAge: 60 * 60 * 24 * 365, // 1 year
48 | }));
49 | }
50 | } else {
51 | // Get current site from request or location
52 | const { host } = process.server ? req.headers : window.location;
53 | sites.some((site) => {
54 | let patterns = site.hostPatterns || null;
55 | if (patterns) {
56 | patterns = patterns.split(',');
57 | return patterns.some((pattern) => {
58 | const regexp = new RegExp(pattern);
59 | if (regexp.test(host)) {
60 | currentSiteId = site.id;
61 | return true;
62 | }
63 | });
64 | }
65 | });
66 | }
67 |
68 | const currentSite = currentSiteId ? sites.find(site => site.id === currentSiteId) : defaultSite;
69 |
70 | multisiteOptions.site = currentSite;
71 |
72 | // CSS vars
73 | const getComputedCssVars = (cssVars) => {
74 | const style = Object.keys(cssVars).map(key => `${key}:${cssVars[key]}`);
75 | return `:root{${style.join(';')}}`;
76 | };
77 | const headStyle = {
78 | id: CSS_VARS_STYLE_ID,
79 | type: 'text/css',
80 | innerHTML: getComputedCssVars(currentSite.cssVars),
81 | };
82 | const styleIndex = ctx.app.head.style.findIndex(style => style.id === CSS_VARS_STYLE_ID);
83 | if (styleIndex !== -1) {
84 | ctx.app.head.style[styleIndex] = headStyle;
85 | } else {
86 | ctx.app.head.style.push(headStyle);
87 | }
88 |
89 | // Meta
90 | if (typeof currentSite.head !== 'undefined') {
91 | ctx.app.head = deepmerge(ctx.app.head, currentSite.head);
92 | }
93 |
94 | // Assets helper
95 | multisiteOptions.asset = (path, site = null) => {
96 | site = site || currentSite.id;
97 | return `/${site}/${path}`;
98 | };
99 |
100 | inject('multisite', multisiteOptions);
101 | };
102 |
--------------------------------------------------------------------------------
/packages/multisite/README.md:
--------------------------------------------------------------------------------
1 | # multisite-module
2 | [](https://npmjs.com/package/@ax2/multisite-module)
3 | [](https://npmjs.com/package/@ax2/multisite-module)
4 | [](https://david-dm.org/ax2inc/multisite-module)
5 | [](https://github.com/airbnb/javascript)
6 |
7 |
8 | > Multisite features for your Nuxt project
9 |
10 | [📖 **Release Notes**](./CHANGELOG.md)
11 |
12 | ## Features
13 |
14 | This module helps you bring multisite features to your Nuxt project. Here are the main features:
15 |
16 | - Current site detection based on host (or query string in development)
17 | - Contextual CSS vars declaration for site-specific theming
18 | - Contextual meta data
19 |
20 | ## Setup
21 |
22 | - Install the module with your favorite package manager.
23 |
24 | ```sh
25 | yarn add @ax2/multisite-module
26 | # Or npm i @ax2/multisite-module
27 | ```
28 |
29 | - Add `@ax2/multisite-module` to `modules` section of `nuxt.config.js`.
30 |
31 | ```js
32 | // nuxt.config.js
33 |
34 | {
35 | modules: [
36 | '@ax2/multisite-module',
37 | ],
38 | }
39 | ```
40 |
41 | - Configure the module as needed by adding a `multisite` key to `nuxt.config.js`.
42 |
43 | ```js
44 | // nuxt.config.js
45 |
46 | {
47 | multisite: {
48 | // Module options
49 | }
50 | }
51 | ```
52 |
53 |
54 | ## Options
55 |
56 | ### debug
57 |
58 | - Type: `Boolean`
59 | - **Default**: `false`
60 |
61 | Set this to `true` to force the module to get the current site from the query string.
62 |
63 | ### sites
64 |
65 | - Type: `Array`
66 |
67 | List of sites.
68 |
69 | ```js
70 | {
71 | multisite: {
72 | sites: [
73 | {
74 | id: 'my-site',
75 | title: 'My awesome site',
76 | isDefault: true,
77 | hostPatterns: 'myawesomesite\.com,myincrediblesite\.(com|org)',
78 | cssVars: {
79 | '--primary-color': '#41B883',
80 | '--secondary-color': '#3B8070',
81 | },
82 | head: {
83 | link: [
84 | { rel: 'icon', type: 'image/x-icon', href: '/my-site/favicon.ico' },
85 | ],
86 | },
87 | },
88 | ],
89 | },
90 | }
91 | ```
92 |
93 | Each item in `sites` can have a few options of its own:
94 |
95 | #### id
96 |
97 | - Type: `Integer|String`
98 |
99 | The site's unique identifier.
100 |
101 | #### isDefault
102 |
103 | - Type: `Boolean`
104 |
105 | Wether this site should be considered as the default one. Any request that cannot be resolved to one of the sites will fallback to the default one.
106 |
107 | #### hostPatterns
108 |
109 | - Type: `String`
110 |
111 | A list of comma-separated patterns to test against requests host in order to enable this site in production.
112 |
113 | #### cssVars
114 |
115 | - Type: `Object`
116 |
117 | CSS vars that should be set when visiting this site.
118 |
119 | #### head
120 |
121 | - Type: `Object`
122 |
123 | This is the same as Nuxt's [head property](https://nuxtjs.org/api/configuration-head#the-head-property), options defined here are merged with the main `head` property definition.
124 |
125 | > NOTE: Functions are not supported here
126 |
127 | ## Usage
128 |
129 | ### Development
130 |
131 | In development, switch from one site to another by adding a `site` query parameter to the URL. The value should be the site's ID as defined in the module's configuration. ie: [http://127.0.0.1:8080/?site=my-site](http://127.0.0.1:8080/?site=my-site)
132 |
133 | Active site is stored in a cookie, so next time you visit [http://127.0.0.1:8080](http://127.0.0.1:8080), active site will be last used one.
134 |
135 | ### Production
136 |
137 | In production, active site is detected by matching request host against the patterns you defined in `hostPatterns` options. ie if you visit [http://myawesomesite.com](http://myawesomesite.com), `my-site` will be set as active site.
138 |
139 | A `$multisite` property is added to the app's context, it contains a few helpers that you can use in any component.
140 |
141 | ### Properties
142 |
143 | #### site
144 |
145 | - Type: `Object`
146 |
147 | The `site` property contains current site's configuration. You could use it to display the current site's title:
148 |
149 | ```vue
150 |
151 |
152 | {{ $multisite.site.title }}
153 |
154 |
155 | ```
156 |
157 | ### Methods
158 |
159 | #### asset
160 |
161 | - Arguments
162 | - `{String} path`: required
163 | - `{Integer|String} site`: optional, defaults to current site ID
164 | - Return: `String`
165 |
166 | Get an asset's path for given site. If no site is specified, defaults to active site.
167 |
168 | ```vue
169 |
170 |
171 |
172 |
173 |
174 |
175 | ```
176 |
177 | > NOTE: It's recommended that you place site-specific assets in a directory named after the site's ID as defined in the module's options. Sites assets directories should be in the static/ directory.
178 |
179 | ## License
180 |
181 | [MIT License](../../LICENSE)
182 |
183 | Copyright (c) Ax2 Inc.
184 |
--------------------------------------------------------------------------------
/packages/gpt-ads/README.md:
--------------------------------------------------------------------------------
1 | # gpt-ads-module
2 | [](https://npmjs.com/package/@ax2/gpt-ads-module)
3 | [](https://npmjs.com/package/@ax2/gpt-ads-module)
4 | [](https://david-dm.org/ax2inc/gpt-ads-module)
5 | [](https://github.com/airbnb/javascript)
6 |
7 | > Google Publisher Tag ads integration for Nuxt
8 |
9 | [📖 **Release Notes**](./CHANGELOG.md)
10 |
11 | ## Features
12 |
13 | Integrate Google Publisher Tag with your Nuxt project.
14 |
15 | ## Setup
16 |
17 | - Install the module with your favorite package manager.
18 |
19 | ```sh
20 | yarn add @ax2/gpt-ads-module
21 | # Or npm i @ax2/gpt-ads-module
22 | ```
23 |
24 | - Add `@ax2/gpt-ads-module` to `modules` section of `nuxt.config.js`.
25 |
26 | ```js
27 | // nuxt.config.js
28 |
29 | {
30 | modules: [
31 | '@ax2/gpt-ads-module',
32 | ],
33 | }
34 | ```
35 |
36 | - Configure the module as needed by adding a `gptAds` key to `nuxt.config.js`.
37 |
38 | ```js
39 | // nuxt.config.js
40 |
41 | {
42 | gptAds: {
43 | // Module options
44 | }
45 | }
46 | ```
47 |
48 | ## Options
49 |
50 | ### networkCode
51 |
52 | - **Type**: `Array|String`: required
53 |
54 | Your network code as found in **Google Ad Manager > Admin > Global Settings**.
55 |
56 | ### debug
57 |
58 | - **Type**: `Boolean`
59 | - **Default**: `false`
60 |
61 | Enable debug mode, when this is `true`, GPT console opens when the app loads.
62 |
63 | ### componentName
64 |
65 | - **Type**: `String`
66 | - **Default**: `'GptAd'`
67 |
68 | Name of the component that the module registers.
69 |
70 | ### individualRefresh
71 |
72 | - **Type**: `Boolean`
73 | - **Default**: `false`
74 |
75 | If enabled, ads won't be fetched on page load but will be refreshed individually as they are mounted.
76 |
77 | ### responsive
78 |
79 | - **Type**: `Boolean`
80 | - **Default**: `false`
81 |
82 | Set to `true` to enable responsive mode for all ads slot. In responsive mode, ad slots listen to window resize events and refresh themselves if a different size mapping matches current window size.
83 |
84 | ### collapseEmptyDivs
85 |
86 | - **Type**: `Boolean`
87 | - **Default**: `false`
88 |
89 | Set to `true` to have empty ad slots collapsed themselves, this can be overridden at slot-level with `collapseEmptyDiv` prop.
90 |
91 | ### ghostMode
92 |
93 | - **Type**: `Boolean`
94 | - **Default**: `false`
95 |
96 | Set to `true` to enable ghost mode. With ghost mode enabled, ad slots aren't displayed and are replaced by empty bordered divs of the size the ads would have if they were displayed. This is useful during development where you might not want to display real ads.
97 |
98 | ### emptyClass
99 |
100 | - **Type**: `String`
101 | - **Default**: `'is-empty'`
102 |
103 | Class to apply to empty ads slots.
104 |
105 | ## Usage
106 |
107 | When the module is enabled, it registers a global Vue component that you can use to display ads in your app. By default, the component's name is **GptAd** but this can be changed via the `componentName` option.
108 |
109 | The component accepts a few props to customize the ads you display.
110 |
111 | ### Props
112 |
113 | #### adUnit
114 |
115 | - Type: `string`: required
116 |
117 | The ad unit for a given ad as defined in Google Ad Manager > Inventory > Ad units.
118 |
119 | #### size
120 |
121 | - Type: `Array|string`: required
122 |
123 | Default size for this ad, can be an array (`[, ]`) or a string (`'x'`).
124 |
125 | To support multiple sizes, either pass an array of arrays (`[[, ], [, ]]`), or a string where dimensions are separated by a comma (`'x,x'`).
126 |
127 | #### sizeMapping
128 |
129 | - Type: `Array`
130 | - Default: `[]`
131 |
132 | Size mapping for this ad. Each item in the list is an array of its own, where the first item is the browser size, and the second is the expected ad's size(s) for the breakpoint.
133 | Sizes should either be arrays in the form `[, ]` or strings in the form `'x'`.
134 |
135 | #### isResponsive
136 |
137 | - Type: `Boolean`
138 | - Default: `<%= options.responsive %>`
139 |
140 | Turn responsive mode on or off for specific ads, defaults to module's `responsive` option.
141 |
142 | #### windowResizeDebounce
143 |
144 | - Type: `Number`
145 | - Default: `300`
146 |
147 | Debounce duration between each window resize handling.
148 |
149 | #### collapseEmptyDiv
150 |
151 | - Type: `Boolean`
152 | - Default: `null`
153 |
154 | Override `collapseEmptyDivs` option at the slot's level.
155 |
156 | ### Examples
157 |
158 |
159 | ```vue
160 |
161 |
170 |
171 | ```
172 |
173 | Equivalent:
174 |
175 |
176 | ```vue
177 |
178 |
187 |
188 | ```
189 |
190 |
191 | ## Development
192 |
193 | - Clone this repository
194 | - Install dependencies using `yarn install` or `npm install`
195 | - Start development server using `npm run dev`
196 |
197 | ## License
198 |
199 | [MIT License](../../LICENSE)
200 |
201 | Copyright (c) Ax2 Inc.
202 |
--------------------------------------------------------------------------------
/packages/gpt-ads/lib/templates/component.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: '<%= options.componentName %>',
3 | data: () => ({
4 | adSlot: null,
5 | mapping: [],
6 | currentSizeMappingIndex: null,
7 | windowResizeListenerDebounce: null,
8 | ghostMode: <%= options.ghostMode %>,
9 | isEmpty: true,
10 | }),
11 | props: {
12 | adUnit: {
13 | type: String,
14 | required: true,
15 | },
16 | size: {
17 | type: [Array, String],
18 | required: true,
19 | },
20 | sizeMapping: {
21 | type: Array,
22 | required: false,
23 | default: () => [],
24 | },
25 | id: {
26 | type: [Number, String],
27 | required: false,
28 | default: () => Math.random().toString(36).substring(5),
29 | },
30 | isResponsive: {
31 | type: Boolean,
32 | required: false,
33 | default: <%= options.responsive %>,
34 | },
35 | windowResizeDebounce: {
36 | type: Number,
37 | required: false,
38 | default: 300,
39 | },
40 | collapseEmptyDiv: {
41 | type: Boolean,
42 | required: false,
43 | default: null,
44 | },
45 | },
46 | computed: {
47 | networkCode() {
48 | const { $gptAds } = this;
49 | return $gptAds ? $gptAds.networkCode : null;
50 | },
51 | adUnitPath() {
52 | const { networkCode, adUnit } = this;
53 | return `/${networkCode}/${adUnit}`;
54 | },
55 | divId() {
56 | const { id } = this;
57 | return `div-gpt-ad-${id}-0`;
58 | },
59 | formattedSize() {
60 | return this.formatSizeList(this.size);
61 | },
62 | style() {
63 | if (this.ghostMode) {
64 | const { formattedSize, currentSizeMappingIndex, mapping } = this;
65 | let baseSize = formattedSize;
66 | if (currentSizeMappingIndex !== null) {
67 | baseSize = mapping[currentSizeMappingIndex][1];
68 | }
69 | const size = Array.isArray(baseSize[0]) ? baseSize[0] : [baseSize[0], baseSize[1]];
70 | const [width, height] = size;
71 | return {
72 | margin: '0 auto',
73 | width: `${width}px`,
74 | height: `${height}px`,
75 | border: '1px solid black',
76 | };
77 | }
78 | return null;
79 | },
80 | },
81 | methods: {
82 | /**
83 | * Formats a given size to make it compatible with GPT
84 | * If size is an Array, it is returned as is
85 | * If size is a string, it is formatted so that 123x456 becomes [123, 456]
86 | *
87 | * @param {Array,string} size The size
88 | * @return {Array} Formatted size
89 | */
90 | formatSize(size) {
91 | if (Array.isArray(size)) {
92 | return size;
93 | }
94 | if (typeof size === 'string') {
95 | return size.split('x').map(value => parseInt(value, 10));
96 | }
97 | return [];
98 | },
99 | /**
100 | * Formats a given list of sizes to make it compatible with GPT API
101 | * If sizesList is an Array, it is returned as is
102 | * If sizesList is a string, it is formatted so that
103 | * 123x456,654x321 becomes [[123, 456], [654, 321]]
104 | *
105 | * @param {Array,string} sizesList The sizes
106 | * @return {Array} Formatted sizes list
107 | */
108 | formatSizeList(sizesList) {
109 | if (Array.isArray(sizesList)) {
110 | return sizesList;
111 | }
112 | if (typeof sizesList === 'string') {
113 | return sizesList
114 | .split(',')
115 | .map(size => this.formatSize(size));
116 | }
117 | return [];
118 | },
119 | /**
120 | * Refresh ad slot
121 | */
122 | refreshSlot() {
123 | googletag.pubads().refresh([this.adSlot]);
124 | },
125 | handleSlotRenderEnded (event) {
126 | if (event.slot.getSlotId().getDomId() !== this.divId) {
127 | return;
128 | }
129 | this.isEmpty = !!event.isEmpty;
130 | },
131 | /**
132 | * Window resize event listener
133 | * Attached only when responsive mode is enabled, it checks wether a different size
134 | * mapping can be activated after resize and forces the slot to be refreshed if it's
135 | * the case
136 | */
137 | handleWindowResize() {
138 | const { windowResizeDebounce } = this;
139 | clearTimeout(this.windowResizeListenerDebounce);
140 | this.windowResizeListenerDebounce = setTimeout(() => {
141 | const currentSizeMappingIndex = this.getCurrentSizeMappingIndex();
142 | if (currentSizeMappingIndex !== this.currentSizeMappingIndex) {
143 | if (!this.ghostMode) {
144 | this.refreshSlot();
145 | }
146 | this.currentSizeMappingIndex = currentSizeMappingIndex;
147 | }
148 | }, windowResizeDebounce);
149 | },
150 | /**
151 | * Gets the current size mapping index
152 | *
153 | * @return {Number} The current size mapping index
154 | */
155 | getCurrentSizeMappingIndex() {
156 | const mapping = this.mapping || [];
157 | let index = null;
158 | mapping.some((size, i) => {
159 | const [browserSize] = size;
160 | const [width, height] = browserSize;
161 | const mediaQuery = `(min-width: ${width}px) and (min-height: ${height}px)`;
162 | if (window.matchMedia(mediaQuery).matches) {
163 | index = i;
164 | return true;
165 | }
166 | return false;
167 | });
168 | return index;
169 | },
170 | },
171 | mounted() {
172 | if (!window.googletag) {
173 | return;
174 | }
175 | const {
176 | ghostMode,
177 | adUnitPath,
178 | divId,
179 | sizeMapping,
180 | isResponsive,
181 | collapseEmptyDiv,
182 | } = this;
183 |
184 |
185 | // Init Ad slot
186 | googletag.cmd.push(() => {
187 | const pubadsService = googletag.pubads()
188 | pubadsService.addEventListener('slotRenderEnded', this.handleSlotRenderEnded);
189 |
190 | const adSlot = googletag
191 | .defineSlot(adUnitPath, this.formattedSize, divId)
192 | .addService(pubadsService);
193 |
194 | // Collapse empty div slot-level override
195 | if (collapseEmptyDiv !== null) {
196 | adSlot.setCollapseEmptyDiv(collapseEmptyDiv);
197 | }
198 |
199 | // Build size mapping if any
200 | if (sizeMapping.length > 0) {
201 | const mapping = googletag.sizeMapping();
202 | sizeMapping.forEach((size) => {
203 | const browserSize = this.formatSize(size[0]);
204 | const adSizes = this.formatSizeList(size[1]);
205 | mapping.addSize(browserSize, adSizes);
206 | this.mapping.push([browserSize, adSizes]);
207 | });
208 | adSlot.defineSizeMapping(mapping.build());
209 | }
210 |
211 | // Init responsive behavior
212 | if (this.sizeMapping.length > 0 && isResponsive) {
213 | const currentSizeMappingIndex = this.getCurrentSizeMappingIndex();
214 | this.currentSizeMappingIndex = currentSizeMappingIndex;
215 | window.addEventListener('resize', this.handleWindowResize);
216 | }
217 |
218 | this.adSlot = adSlot;
219 | this.$gptAds.slots.push(adSlot);
220 |
221 | if (!this.ghostMode) {
222 | googletag.display(divId);
223 | if (this.$gptAds.individualRefresh) {
224 | this.refreshSlot();
225 | }
226 | }
227 | });
228 | },
229 | beforeDestroy() {
230 | if (!googletag) {
231 | return;
232 | }
233 | // Destroy ad slot
234 | googletag.cmd.push(() => {
235 | const destroyed = googletag.destroySlots([this.adSlot]);
236 | });
237 | // Remove window resize listener
238 | window.removeEventListener('resize', this.handleWindowResize);
239 | },
240 | render(h) {
241 | const { divId, style, isEmpty } = this;
242 | let classAttr = isEmpty ? '<%= options.emptyClass %>' : '';
243 |
244 | return h('div', {
245 | style,
246 | attrs: {
247 | id: divId,
248 | class: classAttr,
249 | },
250 | domProps: { innerHTML: '' },
251 | });
252 | },
253 | };
254 |
--------------------------------------------------------------------------------