├── .babelrc ├── .editorconfig ├── .github └── workflows │ ├── analyzer.yml │ └── tests.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierrc ├── .releaserc.json ├── LICENSE ├── README.md ├── README_RU.md ├── bin ├── analyzer.js ├── index.js └── version.js ├── commitlint.config.js ├── example ├── html │ ├── about.html │ ├── contact │ │ └── index.html │ ├── team.html │ └── text.txt ├── html2 │ ├── contact │ │ └── index.html │ └── team.html ├── index.html ├── index.js └── spa │ ├── index.html │ ├── sitemap.xml │ └── static │ ├── css │ ├── app.6fde27bf4e24b3395514fd5ee0329156.css │ └── app.6fde27bf4e24b3395514fd5ee0329156.css.map │ ├── js │ ├── app.8d8d5cefbdfeb5ce0c88.js │ ├── app.8d8d5cefbdfeb5ce0c88.js.map │ ├── manifest.68b98c1982578ae4634f.js │ ├── manifest.68b98c1982578ae4634f.js.map │ ├── vendor.d4d5ba5cda3e6095416f.js │ └── vendor.d4d5ba5cda3e6095416f.js.map │ └── normalize.css ├── jest.config.ts ├── package.json ├── pnpm-lock.yaml ├── preview.png ├── rollup.config.mjs ├── src ├── config.ts ├── index.ts ├── interfaces.ts ├── modules │ ├── analyzer.ts │ ├── input.ts │ ├── logger.ts │ ├── output.ts │ └── scraper.ts ├── rules │ ├── ATagWithRelAttributeRule.ts │ ├── CanonicalLinkRule.ts │ ├── HeadingsStructureRule.ts │ ├── ImgTagWithAltAttributeRule.ts │ ├── MetaBaseRule.ts │ ├── MetaDescriptionRule.ts │ ├── MetaSocialRule.ts │ ├── TitleLengthRule.ts │ ├── config │ │ └── defaults.ts │ └── index.ts ├── server.ts └── shims.d.ts ├── tests ├── index.spec.ts ├── modules │ └── logger.spec.ts └── rules │ ├── ATagWithRelAttritubeRule.spec.ts │ ├── CanonicalLinkRule.spec.ts │ ├── ImgTagWithAltAttritubeRule.spec.ts │ ├── MetaBaseRule.spec.ts │ ├── MetaSocialRule.spec.ts │ └── TitleLengthRule.spec.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/analyzer.yml: -------------------------------------------------------------------------------- 1 | name: SEO analyzer 2 | 3 | on: [push] 4 | 5 | jobs: 6 | seo-analyzer: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Use Node.js 11 | uses: actions/setup-node@v4 12 | with: 13 | node-version: '20.x' 14 | - run: npm i -g seo-analyzer 15 | - run: seo-analyzer -h 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Use Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: '20.x' 15 | - run: npx pnpm i 16 | - run: npx pnpm run build 17 | - run: npx pnpm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | .nyc_output -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .env.local 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .idea 8 | .vscode 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | babel.config.js 14 | preview.png 15 | .editorconfig 16 | .eslintrc.js 17 | .eslintignore 18 | .prettierrc 19 | .babelrc 20 | .github/ 21 | .husky/ 22 | node_modules/ 23 | example/ 24 | src/ 25 | tests/ 26 | coverage 27 | .nyc_output 28 | rollup.config.mjs 29 | tsconfig.json 30 | commitlint.config.js 31 | .releaserc.json 32 | jest.config.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "proseWrap": "never", 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "plugins": ["@semantic-release/commit-analyzer"] 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mad Devs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SEO analyzer — library for searching SEO issues 2 | 3 | [![Developed by Mad Devs](https://maddevs.io/badge-dark.svg)](https://maddevs.io?utm_source=github&utm_medium=madboiler) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | ![Preview](preview.png) 6 | 7 | The library for analyze a HTML files to show all of the SEO defects. 8 | 9 | ## Translations of documentation 10 | 11 | - English 12 | - [Russian](./README_RU.md) 13 | 14 | ## Advantages of this plugin 15 | 16 | - Easy setup. 17 | - Adding custom rules. 18 | - 6 ready-made rules. 19 | - Running the seo-analyzer for SSR applications. 20 | - Running the seo-analyzer for SPA applications. 21 | - Running the seo-analyzer in pipelines(github, gitlab, ...) or pre-push or anywhere else. 22 | - Multiple options for outputting the result. 23 | 24 | ## Why you should use Seo Analyzer 25 | 26 | - **Saves time:** will save you from manually searching for seo problems. 27 | - **Seo Friendly:** will save your project from problems with search engines. 28 | - **It’s free:** we’re happy to share the results of our work. 29 | 30 | ## How to use the CLI 31 | 32 | To use the CLI, you must install the package globally. 33 | 34 | ```sh 35 | npm install -g seo-analyzer 36 | ``` 37 | 38 | ### Usage 39 | 40 | ```sh 41 | seo-analyzer -h 42 | ``` 43 | 44 | | Option | Args | Description | 45 | | --- | --- | --- | 46 | | -h, --help | null | Show all options. | 47 | | -v, --version | null | **Display Application Version:** Displays the current version of the application. | 48 | | -iu, --ignoreUrls | [array] | **Exclude Specific URLs from Analysis:** Excludes certain URLs from analysis to avoid processing unwanted web pages. | 49 | | -if, --ignoreFiles | [array] | **Exclude Specific Files from Analysis:** Allows excluding certain files from analysis, preventing their processing. | 50 | | -ifl, --ignoreFolders | [array] | **Exclude Specific Folders from Analysis:** Excludes specified folders from the analysis process, ignoring all files within those folders. | 51 | | -u, --urls | [array] | **Perform SEO Analysis on Specified URLs:** Conducts SEO analysis for specified URLs, checking their compliance with certain SEO criteria. | 52 | | -f, --files | [array] | **Perform SEO Analysis on Specified Files:** Performs SEO analysis on specified files, ensuring their adherence to optimization standards and rules. | 53 | | -fl, --folder | [array] | **Perform SEO Analysis on Specified Folders:** Analyzes all files within specified folders for compliance with SEO rules and recommendations. | 54 | | -r, --rules | [array] | **Apply Specific SEO Rules for Analysis:** Applies specific SEO rules during analysis, allowing the user to customize the inspection process. By default run all default rules. | 55 | 56 | #### Example of using multiple url analysis 57 | 58 | ```sh 59 | seo-analyzer -u https://maddevs.io https://maddevs.io/blog 60 | ``` 61 | 62 | #### Example of using rules 63 | 64 | ```sh 65 | seo-analyzer -u https://maddevs.io -r titleLengthRule='{ "min": "500" }' 66 | ``` 67 | 68 | Use json format to pass parameters to the rule. 69 | 70 | ## How to use as github action 71 | 72 | To use SEO analyzer as actions on github, you can create a workflow file in .github/workflows/analyzer.yml with the following content: 73 | 74 | ```yml 75 | name: SEO analyzer 76 | 77 | on: [push] 78 | 79 | jobs: 80 | seo-analyzer: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | - name: Use Node.js 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: '20.x' 88 | - run: npm i -g seo-analyzer 89 | - run: seo-analyzer -u https://maddevs.io 90 | ``` 91 | 92 | In the last step, you can specify the url you want to analyse. 93 | 94 | ## How to use as API 95 | 96 | Install to the project. 97 | 98 | ```sh 99 | npm install -D seo-analyzer 100 | ``` 101 | 102 | ## Usage 103 | 104 | ### Getting started 105 | 106 | Setting up the SEO analyzer is as simple as possible. It will look something like this: 107 | 108 | ```js 109 | const SeoAnalyzer = require('seo-analyzer'); 110 | 111 | new SeoAnalyzer() 112 | .inputFiles() 113 | .addRule() 114 | .addRule() 115 | .outputConsole() 116 | .run(); 117 | ``` 118 | 119 | Next I will show you some examples. 120 | 121 | #### One way: analyze the url list and output the report to the console 122 | 123 | ```js 124 | const SeoAnalyzer = require('seo-analyzer'); 125 | 126 | new SeoAnalyzer() 127 | .inputUrls(['https://maddevs.io', 'https://maddevs.io/blog']) 128 | .addRule('imgTagWithAltAttributeRule') 129 | .outputConsole() 130 | .run(); 131 | ``` 132 | 133 | #### Two way: file analysis for SPA application and log report to console 134 | 135 | ```js 136 | const SeoAnalyzer = require('seo-analyzer'); 137 | 138 | new SeoAnalyzer() 139 | .ignoreUrls(['/404', '/login']) 140 | .inputSpaFolder('/dist', 'sitemapindex.xml', 3000) 141 | .addRule('imgTagWithAltAttributeRule') 142 | .outputConsole() 143 | .run(); 144 | ``` 145 | 146 | #### Three way: read a list HTML files and log report to console 147 | 148 | ```js 149 | const SeoAnalyzer = require('seo-analyzer'); 150 | 151 | new SeoAnalyzer() 152 | .inputFiles(['index.html', 'about.html']) 153 | .addRule('imgTagWithAltAttributeRule') 154 | .outputConsole(); 155 | ``` 156 | 157 | #### Fourth way: read a folders with HTML files and log report to console 158 | 159 | ```js 160 | const SeoAnalyzer = require('seo-analyzer'); 161 | 162 | new SeoAnalyzer() 163 | .inputFolders(['dist', 'src']) 164 | .addRule('imgTagWithAltAttributeRule') 165 | .outputConsole() 166 | .run(); 167 | ``` 168 | 169 | #### Fifth way: read a folders with HTML files and return json 170 | 171 | ```js 172 | const SeoAnalyzer = require('seo-analyzer'); 173 | 174 | new SeoAnalyzer() 175 | .inputFolders(['dist', 'src']) 176 | .addRule('imgTagWithAltAttributeRule') 177 | .outputJson(json => console.log(json)) 178 | .run(); 179 | ``` 180 | 181 | #### Sixth way: ignore subfolder "test" and 404.html in folder "src" and return js object 182 | 183 | ```js 184 | const SeoAnalyzer = require('seo-analyzer'); 185 | 186 | new SeoAnalyzer() 187 | .ignoreFolders(['src/test']) 188 | .ignoreFiles(['src/404.html']) 189 | .inputFolders(['dist', 'src']) 190 | .addRule('imgTagWithAltAttributeRule') 191 | .outputObject(obj => console.log(obj)) 192 | .run(); 193 | ``` 194 | 195 | #### Seventh way: Input an HTML string directly and log report to console 196 | 197 | ```js 198 | const SeoAnalyzer = require('seo-analyzer'); 199 | 200 | new SeoAnalyzer() 201 | .inputHTMLString( 202 | '

title

content

' 203 | ) 204 | .addRule('imgTagWithAltAttributeRule') 205 | .outputConsole() 206 | .run(); 207 | ``` 208 | 209 | ## Available methods: 210 | 211 | | Method | Params | Description | 212 | | --- | --- | --- | 213 | | ignoreFiles | ['dist/about.html'] | This method expects an array of files to ignore before analysis. | 214 | | ignoreFolders | ['dist/ignore'] | This method expects an array of folders to ignore before analysis. | 215 | | ignoreUrls | ['/404'] | This method expects an array of urls to ignore before analysis. | 216 | | inputFiles | ['dist/index.html'] | This method expects an array of html files. | 217 | | inputUrls | ['https://maddevs.io'] | This method expects an array of urls to analyze. | 218 | | inputFolders | ['dist', 'src'] | This method expects an array of folders with html files. | 219 | | inputSpaFolder | '/dist', 'sitemap.xml', 3000 | This method expects an string of folder with SPA builded files to production & port for run server. | 220 | | inputHTMLString | ['example'] | This method expects a string containing HTML. | 221 | | addRule | function(dom) {} | This method adds a custom rule to check incoming HTML files. | 222 | | outputObject | function(obj) {} | This method will return the report as a javascript object. | 223 | | outputJson | function(json) {} | This method will return the report in JSON format. | 224 | | outputConsole | null | This method must be used at the very end of the chain, because it completes the process in the console. | 225 | 226 | ## List of rules that are available by default 227 | 228 | Below are the rules that are executed for each file transferred to Seo Analyzer. They are disabled by default and must be added. 229 | 230 | ### Title Length Rule 231 | 232 | Checks the length of tag ``. Two parameters are accepted: 233 | 234 | - **min:** minimum length of the header 235 | - **max:** maximum length of the header 236 | 237 | ```js 238 | .addRule('titleLengthRule', { min: 10, max: 50 }) 239 | ``` 240 | 241 | ### Img Tag With Alt Attribute Rule 242 | 243 | Checks if all `<img>` tags have alt="" attribute. 244 | 245 | ```js 246 | .addRule('imgTagWithAltAttributeRule') 247 | ``` 248 | 249 | ### `<a>` Tag With Rel Attribute Rule 250 | 251 | Checks if all `<a>` tags have rel="" attribute. 252 | 253 | ```js 254 | .addRule('aTagWithRelAttributeRule') 255 | ``` 256 | 257 | ### Meta Base Rule 258 | 259 | Checks if the specified **basic** meta tags are present on the page. Accepts one parameter: 260 | 261 | - **list:** list of required meta tags 262 | 263 | ```js 264 | .addRule('metaBaseRule', { list: ['description', 'viewport'] }) 265 | ``` 266 | 267 | ### Meta Social Rule 268 | 269 | Checks if the specified **social** meta tags are present on the page. Accepts one parameter: 270 | 271 | - **properties:** list of required meta tags 272 | 273 | ```js 274 | .addRule('metaSocialRule', { 275 | properties: [ 276 | 'og:url', 277 | 'og:type', 278 | 'og:site_name', 279 | 'og:title', 280 | 'og:description', 281 | 'og:image', 282 | 'og:image:width', 283 | 'og:image:height', 284 | 'twitter:card', 285 | 'twitter:text:title', 286 | 'twitter:description', 287 | 'twitter:image:src', 288 | 'twitter:url' 289 | ], 290 | }) 291 | ``` 292 | 293 | ### Canonical Link Rule 294 | 295 | Checks if a canonical link exists on the page. 296 | 297 | ```js 298 | .addRule('canonicalLinkRule') 299 | ``` 300 | 301 | ### Add custom rule 302 | 303 | A custom rule is a function that takes a DOM tree argument. 304 | 305 | ```js 306 | function customRule(dom) { 307 | return new Promise(async (resolve, reject) => { 308 | const paragraph = dom.window.document.querySelector('p'); 309 | if (paragraph) { 310 | resolve(''); 311 | } else { 312 | reject('Not found <p> tags'); 313 | } 314 | }); 315 | } 316 | 317 | ... 318 | .addRule(customRule) 319 | ... 320 | ``` 321 | 322 | ## Licensing 323 | 324 | MIT License 325 | 326 | Copyright (c) 2024 Mad Devs 327 | 328 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 329 | 330 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 331 | 332 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 333 | -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # SEO анализатор - библиотека для поиска SEO ошибок 2 | 3 | [![Developed by Mad Devs](https://maddevs.io/badge-dark.svg)](https://maddevs.io?utm_source=github&utm_medium=madboiler) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | ![Preview](preview.png) 6 | 7 | SEO анализатор это инструмент, который поможет отлавливать SEO дефекты на разных стадиях разработки. Основная задача — анализ DOM дерева с целью обнаружения SEO дефектов. 8 | 9 | ## Переводы документации 10 | 11 | - [English](./README.md) 12 | - Russian 13 | 14 | ## Преимущества этого плагина 15 | 16 | - Простая настройка. 17 | - Добавление собственных правил. 18 | - 6 готовых и популярных правил. 19 | - Запуск анализатора для SSR приложений. 20 | - Запуск анализатора для SPA приложений. 21 | - Запуск анализатора в конвейерах (github, gitlab, ...), pre-push и т.д. 22 | - Несколько вариантов вывода результата. 23 | 24 | ## Почему вам следует использовать Seo Analyzer? 25 | 26 | - **Экономит время:** избавит вас от ручного поиска проблем, которые влияют на поисковую оптимизацию. 27 | - **Seo Friendly:** держит ваш проект валидным и дружелюбным к поисковым роботам путём обнаружения дефектов на страницах вашего сайта. 28 | - **Это бесплатно:** мы рады поделиться результатами своей работы. 29 | 30 | ## Используйте как CLI 31 | 32 | Для использования SEO анализатора в консоли необходимо установить плагин глобально. 33 | 34 | ```sh 35 | npm install -g seo-analyzer 36 | ``` 37 | 38 | ### Информация о параметрах 39 | 40 | ```sh 41 | seo-analyzer -h 42 | ``` 43 | 44 | | Option | Args | Description | 45 | | --- | --- | --- | 46 | | -h, --help | null | Показать справку. | 47 | | -v, --version | null | **Display Application Version:** Выводит информацию о текущей версии программы. | 48 | | -iu, --ignoreUrls | [array] | **Exclude Specific URLs from Analysis:** Исключает определенные URL из анализа для предотвращения обработки нежелательных веб-страниц. | 49 | | -if, --ignoreFiles | [array] | **Exclude Specific Files from Analysis:** Позволяет исключить из анализа определенные файлы, предотвращая их обработку. | 50 | | -ifl, --ignoreFolders | [array] | **Exclude Specific Folders from Analysis:** Исключает указанные папки из процесса анализа, игнорируя все файлы внутри этих папок. | 51 | | -u, --urls | [array] | **Perform SEO Analysis on Specified URLs:** Выполняет SEO-анализ для указанных URL-адресов, проверяя их на соответствие определенным SEO-критериям. | 52 | | -f, --files | [array] | **Perform SEO Analysis on Specified Files:** Производит анализ SEO для указанных файлов, обеспечивая их соответствие стандартам и правилам оптимизации. | 53 | | -fl, --folder | [array] | **Perform SEO Analysis on Specified Folders:** Анализирует все файлы в указанных папках на предмет соответствия SEO-правилам и рекомендациям. | 54 | | -r, --rules | [array] | **Apply Specific SEO Rules for Analysis:** Применяет определенные правила SEO при анализе, позволяя пользователю кастомизировать процесс проверки. Поумолчанию запускаются все дефолтные правила. | 55 | 56 | #### Пример использования с множеством url 57 | 58 | ```sh 59 | seo-analyzer -u https://maddevs.io https://maddevs.io/blog 60 | ``` 61 | 62 | #### Пример использования правил 63 | 64 | ```sh 65 | seo-analyzer -u https://maddevs.io -r titleLengthRule='{ "min": "500" }' 66 | ``` 67 | 68 | Для передачи параметров в правилах используйте json формат. 69 | 70 | ## Используйте как github-action 71 | 72 | Чтобы использовать SEO-анализатор как github-action, вам нужно создать файл в папке .github/workflows/analyzer.yml со следующим содержанием: 73 | 74 | ```yml 75 | name: SEO analyzer 76 | 77 | on: [push] 78 | 79 | jobs: 80 | seo-analyzer: 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v4 84 | - name: Use Node.js 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: '20.x' 88 | - run: npm i -g seo-analyzer 89 | - run: seo-analyzer -u https://maddevs.io 90 | ``` 91 | 92 | На последнем шаге вы можете указать url, который хотите проанализировать. 93 | 94 | ## Используйте как API 95 | 96 | Устанавливаем плагин в ваш проект 97 | 98 | ```sh 99 | npm install -D seo-analyzer 100 | ``` 101 | 102 | ## Применение 103 | 104 | ### Начало 105 | 106 | Настройка SEO анализатора максимально проста. Выглядеть она будет примерно так: 107 | 108 | ```js 109 | const SeoAnalyzer = require('seo-analyzer'); 110 | 111 | new SeoAnalyzer() 112 | .inputFiles(<array>) 113 | .addRule(<function>) 114 | .addRule(<function>) 115 | .outputConsole() 116 | .run(); 117 | ``` 118 | 119 | Далее покажу несколько примеров. 120 | 121 | #### Способ №1: анализ url-адресов и вывод отчета в консоль 122 | 123 | ```js 124 | const SeoAnalyzer = require('seo-analyzer'); 125 | 126 | new SeoAnalyzer() 127 | .inputUrls(['https://maddevs.io', 'https://maddevs.io/blog']) 128 | .addRule('imgTagWithAltAttributeRule') 129 | .outputConsole() 130 | .run(); 131 | ``` 132 | 133 | #### Способ №2: анализ страниц для SPA приложения и вывод отчета в консоль 134 | 135 | ```js 136 | const SeoAnalyzer = require('seo-analyzer'); 137 | 138 | new SeoAnalyzer() 139 | .ignoreUrls(['/404', '/login']) 140 | .inputSpaFolder('/dist', 'sitemap.xml', 3000) 141 | .addRule('imgTagWithAltAttributeRule') 142 | .outputConsole() 143 | .run(); 144 | ``` 145 | 146 | #### Способ №3: анализ список HTML-файлов и вывод отчета в консоль 147 | 148 | ```js 149 | const SeoAnalyzer = require('seo-analyzer'); 150 | 151 | new SeoAnalyzer() 152 | .inputFiles(['index.html', 'about.html']) 153 | .addRule('imgTagWithAltAttributeRule') 154 | .outputConsole() 155 | .run(); 156 | ``` 157 | 158 | #### Способ №4: анализ папок с HTML-файлами и вывод отчета в консоль 159 | 160 | ```js 161 | const SeoAnalyzer = require('seo-analyzer'); 162 | 163 | new SeoAnalyzer() 164 | .inputFolders(['dist', 'src']) 165 | .addRule('imgTagWithAltAttributeRule') 166 | .outputConsole() 167 | .run(); 168 | ``` 169 | 170 | #### Способ №5: анализ папок с HTML-файлами и вывод отчета в виде JSON 171 | 172 | ```js 173 | const SeoAnalyzer = require('seo-analyzer'); 174 | 175 | new SeoAnalyzer() 176 | .inputFolders(['dist', 'src']) 177 | .addRule('imgTagWithAltAttributeRule') 178 | .outputJson(json => console.log(json)) 179 | .run(); 180 | ``` 181 | 182 | #### Способ №6: игнорировать подпапку "test" и 404.html в папке "src" и вернуть объект js 183 | 184 | ```js 185 | const SeoAnalyzer = require('seo-analyzer'); 186 | 187 | new SeoAnalyzer() 188 | .ignoreFolders(['src/test']) 189 | .ignoreFiles(['src/404.html']) 190 | .inputFolders(['dist', 'src']) 191 | .addRule('imgTagWithAltAttributeRule') 192 | .outputObject(obj => console.log(obj)) 193 | .run(); 194 | ``` 195 | 196 | #### Способ №7: Ввод HTML-строки напрямую и вывод отчета в консоль 197 | 198 | ```js 199 | const SeoAnalyzer = require('seo-analyzer'); 200 | 201 | new SeoAnalyzer() 202 | .inputHTMLString( 203 | '<!DOCTYPE html><html><body><h1>title</h1><p>content</p></body></html>' 204 | ) 205 | .addRule('imgTagWithAltAttributeRule') 206 | .outputConsole() 207 | .run(); 208 | ``` 209 | 210 | ## Доступные методы: 211 | 212 | | Метод | Параметры | Описание | 213 | | --- | --- | --- | 214 | | ignoreFiles | ['dist/about.html'] | Массив файлов, которые будут проигнорированны во время анализа. | 215 | | ignoreFolders | ['dist/ignore'] | Массив папок с файлами, которые будут проигнорированны во время анализа. | 216 | | ignoreUrls | ['/404'] | Массив URL-адресов, которые будут проигнорированны во время анализа. | 217 | | inputFiles | ['dist/index.html'] | Массив файлов, которые нужно анализировать. | 218 | | inputUrls | ['https://maddevs.io'] | Массив URL-адресов, которые нужно анализировать. | 219 | | inputFolders | ['dist', 'src'] | Массив папкок с файлами, которые нужно анализировать. | 220 | | inputSpaFolder | '/dist', 'sitemap.xml', 3000 | Метод для запуска анализаторв для SPA приложений. Ожидает папку с финальными кодом приложения и порт на котором запустится анализатор. | 221 | | inputHTMLString | ['<html>example</html>'] | Этот метод ожидает строку, содержащую HTML. | 222 | | addRule | function(dom) {} | Метод для добавления встроенных правил или собственных. | 223 | | outputObject | function(obj) {} | Метод для вывода результата. Вернёт js объект. | 224 | | outputJson | function(json) {} | Метод для вывода результата. Вернёт JSON. | 225 | | outputConsole | null | Метод для вывода результата. Вернёт результат в консоль. Использовать только в самом конце цепочки, потому что он завершает процесс и цепочка методов ниже не сработает | 226 | 227 | ## Список правил, доступных по умолчанию 228 | 229 | Ниже приведены правила, которые выполняются для каждого файла передаваемого в Seo Analyzer. По умолчанию они отключены и должны быть добавлены. 230 | 231 | ### Правило длины заголовка 232 | 233 | Проверяет длину тега `<title>`. Принимаются два параметра: 234 | 235 | - **min:** минимальная длина заголовка 236 | - **max:** максимальная длина заголовка 237 | 238 | ```js 239 | .addRule('titleLengthRule', { min: 10, max: 50 }) 240 | ``` 241 | 242 | ### Правило теги Img с атрибутом Alt 243 | 244 | Проверяет, все ли теги `<img>` имеют текст в атрибуте `alt=""`. 245 | 246 | ```js 247 | .addRule('imgTagWithAltAttributeRule') 248 | ``` 249 | 250 | ### `<a>` Правило тега `<a>` с атрибутом Rel 251 | 252 | Проверяет, все ли теги `<a>` имеют атрибут `rel=""` 253 | 254 | ```js 255 | .addRule('aTagWithRelAttributeRule') 256 | ``` 257 | 258 | ### Правило метаданных 259 | 260 | Проверяет, присутствуют ли на странице указанные **базовые** мета-теги. Принимает один параметр: 261 | 262 | - **list:** список необходимых метатегов 263 | 264 | ```js 265 | .addRule('metaBaseRule', { list: ['description', 'viewport'] }) 266 | ``` 267 | 268 | ### Правило мета-тегов для социальных сетей 269 | 270 | Проверяет, присутствуют ли на странице указанные **социальные** мета-теги. Принимает один параметр: 271 | 272 | - **properties:** список необходимых мета-тегов 273 | 274 | ```js 275 | .addRule('metaSocialRule', { 276 | properties: [ 277 | 'og:url', 278 | 'og:type', 279 | 'og:site_name', 280 | 'og:title', 281 | 'og:description', 282 | 'og:image', 283 | 'og:image:width', 284 | 'og:image:height', 285 | 'twitter:card', 286 | 'twitter:text:title', 287 | 'twitter:description', 288 | 'twitter:image:src', 289 | 'twitter:url' 290 | ], 291 | }) 292 | ``` 293 | 294 | ### Правило канонической ссылки 295 | 296 | Проверяет, существует ли на странице каноническая ссылка. 297 | 298 | ```js 299 | .addRule('canonicalLinkRule') 300 | ``` 301 | 302 | ### Добавить собственное правило 303 | 304 | Пользовательское правило - это всего лишь функция, которая принимает один аргумент в виде DOM дерева. 305 | 306 | ```js 307 | function customRule(dom) { 308 | return new Promise(async (resolve, reject) => { 309 | const paragraph = dom.window.document.querySelector('p'); 310 | if (paragraph) { 311 | resolve(''); 312 | } else { 313 | reject('Not found <p> tags'); 314 | } 315 | }); 316 | } 317 | 318 | ... 319 | .addRule(customRule) 320 | ... 321 | ``` 322 | 323 | ## Лицензирование 324 | 325 | Лицензия MIT 326 | 327 | Авторское право (c) 2024 Mad Devs 328 | 329 | Настоящим предоставляется бесплатное разрешение любому лицу, получившему копию этого программного обеспечения и связанных файлов документации («Программное обеспечение»), на работу с Программным обеспечением без ограничений, включая, помимо прочего, права на использование, копирование, изменение, объединение, публикацию, распространение, сублицензирование и/или продажу копии Программного обеспечения и разрешение лицам, которым предоставляется Программное обеспечение, делать это при соблюдении следующих условий: 330 | 331 | Вышеупомянутое уведомление об авторских правах и это уведомление о разрешении должны быть включены во все копии или существенные части Программного обеспечения. 332 | 333 | ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ГАРАНТИИ КОММЕРЧЕСКОЙ ПРИГОДНОСТИ, ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ И НЕЗАЩИТЫ ОТ ПРАКТИКИ. НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ВЛАДЕЛЬЦЫ АВТОРСКИХ ПРАВ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБЫЕ ПРЕТЕНЗИИ, УБЫТКИ ИЛИ ДРУГИЕ ОТВЕТСТВЕННОСТИ, КОТОРЫЕ ВОЗНИКЛИ В РЕЗУЛЬТАТЕ ДОГОВОРА, ПРАКТИЧЕСКИХ ПРАВ ИЛИ ИНЫХ СЛУЧАЕВ, ВНУТРИ ИЛИ В СВЯЗИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ИЛИ ДРУГИМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. 334 | -------------------------------------------------------------------------------- /bin/analyzer.js: -------------------------------------------------------------------------------- 1 | const colors = require('colors'); 2 | const SeoAnalyzer = require('../dist/index.js'); 3 | 4 | const INVALID_RULE_MESSAGE = `❌ Incorrect format of the rule value. The value must be in JSON format. \n❌ For example: -r metaSocialRule='{ "properties": ["og:url"] }'`; 5 | 6 | const IGNORES = { 7 | ignoreFiles: 'ignoreFiles', 8 | ignoreFolders: 'ignoreFolders', 9 | ignoreUrls: 'ignoreUrls' 10 | }; 11 | 12 | const INPUTS = { 13 | files: 'inputFiles', 14 | folders: 'inputFolders', 15 | urls: 'inputUrls' 16 | }; 17 | 18 | function formatRuleParams(input) { 19 | if (!input) return null; 20 | try { 21 | const jsonObj = JSON.parse(input); 22 | return jsonObj; 23 | } catch { 24 | console.error(`${colors.red(INVALID_RULE_MESSAGE)}`); 25 | process.exit(1); 26 | } 27 | } 28 | 29 | function detectOptions(options, obj) { 30 | return Object.keys(obj) 31 | .filter(key => options[key]) 32 | .map(key => ({ 33 | name: obj[key], 34 | value: options[key] 35 | })); 36 | } 37 | 38 | function detectRules(options) { 39 | const rules = options.rules || []; 40 | return rules.map(rule => ({ 41 | name: rule.split('=')[0], 42 | value: formatRuleParams(rule.split('=')[1]) 43 | })); 44 | } 45 | 46 | module.exports = options => { 47 | const analyzer = new SeoAnalyzer(); 48 | 49 | const ignores = detectOptions(options, IGNORES); 50 | for (const { name, value } of ignores) { 51 | analyzer[name](value); 52 | } 53 | 54 | const inputs = detectOptions(options, INPUTS); 55 | for (const { name, value } of inputs) { 56 | analyzer[name](value); 57 | } 58 | 59 | const rules = detectRules(options); 60 | const resultRules = rules.length 61 | ? rules 62 | : Object.keys(analyzer.getDefaultRules()); 63 | for (const rule of resultRules) { 64 | analyzer.addRule(rule?.name || rule, rule?.value); 65 | } 66 | 67 | analyzer.outputConsole().run(); 68 | }; 69 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { Command } = require('commander'); 3 | const version = require('./version'); 4 | const analyzer = require('./analyzer'); 5 | 6 | const program = new Command(); 7 | 8 | program 9 | .option( 10 | '-v, --version', 11 | 'Display Application Version: Displays the current version of the application.' 12 | ) 13 | // IGNORES 14 | .option( 15 | '-iu, --ignore-urls <ignoreUrls...>', 16 | 'Exclude Specific URLs from Analysis: Excludes certain URLs from analysis to avoid processing unwanted web pages.' 17 | ) 18 | .option( 19 | '-if, --ignore-files <ignoreFiles...>', 20 | 'Exclude Specific Files from Analysis: Allows excluding certain files from analysis, preventing their processing.' 21 | ) 22 | .option( 23 | '-ifl, --ignore-folders <ignoreFolders...>', 24 | 'Exclude Specific Folders from Analysis: Excludes specified folders from the analysis process, ignoring all files within those folders.' 25 | ) 26 | // INPUTS 27 | .option( 28 | '-u, --urls <urls...>', 29 | 'Perform SEO Analysis on Specified URLs: Conducts SEO analysis for specified URLs, checking their compliance with certain SEO criteria.' 30 | ) 31 | .option( 32 | '-f, --files <files...>', 33 | 'Perform SEO Analysis on Specified Files: Performs SEO analysis on specified files, ensuring their adherence to optimization standards and rules.' 34 | ) 35 | .option( 36 | '-fl, --folders <folders...>', 37 | 'Perform SEO Analysis on Specified Folders: Analyzes all files within specified folders for compliance with SEO rules and recommendations.' 38 | ) 39 | // RULES 40 | .option( 41 | '-r, --rules <rules...>', 42 | 'Apply Specific SEO Rules for Analysis: Applies specific SEO rules during analysis, allowing the user to customize the inspection process.' 43 | ) 44 | .parse(process.argv); 45 | 46 | const options = program.opts(); 47 | 48 | if (!Object.keys(options).length) { 49 | program.outputHelp(); 50 | } else if (options.version) { 51 | version(); 52 | } else { 53 | analyzer(options); 54 | } 55 | -------------------------------------------------------------------------------- /bin/version.js: -------------------------------------------------------------------------------- 1 | const package = require('../package.json'); 2 | module.exports = () => { 3 | console.log(package.version); 4 | }; 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [0, 'always', 100] 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /example/html/about.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 | <meta name="keywords" content="html, css"> 8 | <meta name="description" content="Description"> 9 | <meta property="og:url" content="#"> 10 | <meta property="twitter:url" content="#"> 11 | <meta property="twitter:image:src" content="#"> 12 | <meta property="twitter:text:title" content="#"> 13 | <meta property="twitter:card" content="#"> 14 | <meta property="twitter:description" content="#"> 15 | <meta property="og:type" content="#"> 16 | <meta property="og:site_name" content="#"> 17 | <meta property="og:title" content="#"> 18 | <meta property="og:description" content="#"> 19 | <meta property="og:image" content="#"> 20 | <meta property="og:image:width" content="#"> 21 | <meta property="og:image:height" content="#"> 22 | <link rel="canonical" href="https://google.com/"> 23 | <title>Document title 24 | 25 | 26 |

Title

27 | 28 | -------------------------------------------------------------------------------- /example/html/contact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Contact page

11 | 12 | -------------------------------------------------------------------------------- /example/html/team.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Team page

10 | 11 | -------------------------------------------------------------------------------- /example/html/text.txt: -------------------------------------------------------------------------------- 1 | Simple text -------------------------------------------------------------------------------- /example/html2/contact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Contact page

11 | 12 | -------------------------------------------------------------------------------- /example/html2/team.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Team page

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | D 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Title 1

14 |

Title 3

15 |

Title 2

16 |

17 | Lorem ipsum, dolor sit amet consectetur adipisicing 18 | elit. Nesciunt recusandae animi esse 19 | sunt enim iusto repellendus expedita sit provident delectus? 20 |

21 | Alt 22 | 23 | 24 | Link 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const SeoAnalyzer = require('../dist/index.js'); 2 | 3 | // --------- Custom rules ------------ // 4 | function customRule(dom) { 5 | return new Promise((resolve, reject) => { 6 | const paragraph = dom.window.document.querySelector('p'); 7 | if (paragraph) { 8 | resolve(''); 9 | } else { 10 | reject('No

tags found'); 11 | } 12 | }); 13 | } 14 | // -------------------------------- // 15 | 16 | new SeoAnalyzer() 17 | // ------- Ignore methods ------- // 18 | .ignoreFolders(['example/html/contact']) 19 | .ignoreFiles(['example/html/team.html']) 20 | .ignoreUrls(['/#/product/2']) 21 | // ------- Input methods -------- // 22 | // .inputFolders(['example']) 23 | .inputUrls(['https://maddevs.io']) 24 | // .inputFiles(['example/html/index.html']) 25 | // .inputSpaFolder('example/spa', 'sitemap.xml') 26 | // .inputHTMLStrings([ 27 | // { 28 | // source: '/myExamplePage', 29 | // text: '

title

content

' 30 | // } 31 | // ]) 32 | 33 | // ------ Default rules -------- // 34 | .useRule('titleLengthRule', { min: 10, max: 50 }) 35 | .useRule('metaBaseRule', { names: ['title', 'description'] }) 36 | .useRule('metaSocialRule', { 37 | properties: [ 38 | 'og:url', 39 | 'og:type', 40 | 'og:site_name', 41 | 'og:title', 42 | 'og:description', 43 | 'og:image', 44 | 'og:image:width', 45 | 'og:image:height', 46 | 'twitter:card', 47 | 'twitter:text:title', 48 | 'twitter:description', 49 | 'twitter:image:src', 50 | 'twitter:url' 51 | ] 52 | }) 53 | .useRule('imgTagWithAltAttributeRule') 54 | .useRule('aTagWithRelAttributeRule') 55 | .useRule('canonicalLinkRule') 56 | // Custom rules 57 | .addRule(customRule) 58 | // ------- Output methods ------- // 59 | // .outputObject(obj => console.log(obj)) 60 | // .outputJson(json => console.log(json)) 61 | .outputConsole() 62 | .run(); 63 | -------------------------------------------------------------------------------- /example/spa/index.html: -------------------------------------------------------------------------------- 1 | vue-spa
-------------------------------------------------------------------------------- /example/spa/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | https://site.com/ 8 | 2021-09-29T00:00:00.000Z 9 | daily 10 | 1.0 11 | 12 | 13 | https://site.com/#/cart/ 14 | 2021-09-29T00:00:00.000Z 15 | daily 16 | 1.0 17 | 18 | 19 | https://site.com/#/product/1 20 | 2021-09-29T00:00:00.000Z 21 | daily 22 | 1.0 23 | 24 | 25 | https://site.com/#/product/2 26 | 2021-09-29T00:00:00.000Z 27 | daily 28 | 1.0 29 | 30 | 31 | https://site.com/#/product/3 32 | 2021-09-29T00:00:00.000Z 33 | daily 34 | 1.0 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/spa/static/css/app.6fde27bf4e24b3395514fd5ee0329156.css: -------------------------------------------------------------------------------- 1 | body{font-family:HelveticaNeue,Helvetica Neue,Helvetica,Arial,sans-serif;color:#333}.app{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.header{border-bottom:1px solid #b3b3b3}.header,.page{width:100%;padding:30px 50px}.help-text{margin-top:20px;font-size:12px}.menu-links a{display:inline-block;text-decoration:none;color:#555;margin-right:30px}.cart{width:600px}.checkout-table{width:100%}.checkout-table th{text-align:left;padding:15px 0;border-bottom:1px solid #aaa}.checkout-table td{padding:8px 0}.checkout-button{float:right;width:120px;height:40px;margin-top:20px}.total td{border-top:1px solid #aaa;padding-top:10px}.product{padding:10px 0;border-bottom:1px solid #eee;width:400px}.title{color:#312377}.price{float:right}.product-container{margin-bottom:50px}.product-item{margin:10px;width:500px;height:400px;border-bottom:1px solid #aaa}.back-link{font-size:20px}.product-title{padding-top:120px;text-align:center;margin:0 auto;font-size:26px}.product-details{margin-top:120px}.inventory{float:left;font-size:20px;margin-top:15px}.add-button{float:right;width:140px;height:50px} -------------------------------------------------------------------------------- /example/spa/static/css/app.6fde27bf4e24b3395514fd5ee0329156.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./src/App.vue","webpack:///./src/components/HeaderBar.vue","webpack:///./src/pages/CartPage.vue","webpack:///./src/pages/HomePage.vue","webpack:///./src/pages/ProductPage.vue"],"names":[],"mappings":"AACA,KACE,oEACA,UAAY,CAEd,KACE,oBACA,oBACA,aACA,4BACA,6BACI,0BACI,sBACR,UAAY,CAEd,QACE,+BAAiC,CAInC,cAHE,WACA,iBAAmB,CAMrB,WACE,gBACA,cAAgB,CCzBlB,cACE,qBACA,qBACA,WACA,iBAAmB,CCJrB,MACE,WAAa,CAEf,gBACE,UAAY,CAEd,mBACE,gBACA,eACA,4BAA8B,CAEhC,mBACE,aAAiB,CAEnB,iBACE,YACA,YACA,YACA,eAAiB,CAEnB,UACE,0BACA,gBAAkB,CCtBpB,SACE,eACA,6BACA,WAAa,CAEf,OACE,aAAe,CAEjB,OACE,WAAa,CAEf,mBACE,kBAAoB,CCZtB,cACE,YACA,YACA,aACA,4BAA8B,CAEhC,WACE,cAAgB,CAElB,eACE,kBACA,kBACA,cACA,cAAgB,CAElB,iBACE,gBAAkB,CAEpB,WACE,WACA,eACA,eAAiB,CAEnB,YACE,YACA,YACA,WAAa","file":"static/css/app.6fde27bf4e24b3395514fd5ee0329156.css","sourcesContent":["\nbody {\n font-family: \"HelveticaNeue\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n color: #333;\n}\n.app {\n display: -webkit-box;\n display: -ms-flexbox;\n display: flex;\n -webkit-box-orient: vertical;\n -webkit-box-direction: normal;\n -ms-flex-direction: column;\n flex-direction: column;\n width: 100%;\n}\n.header {\n border-bottom: 1px solid #b3b3b3;\n width: 100%;\n padding: 30px 50px;\n}\n.page {\n width: 100%;\n padding: 30px 50px;\n}\n.help-text {\n margin-top: 20px;\n font-size: 12px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/App.vue","\n.menu-links a {\n display: inline-block;\n text-decoration: none;\n color: #555;\n margin-right: 30px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/HeaderBar.vue","\n.cart {\n width: 600px;\n}\n.checkout-table {\n width: 100%;\n}\n.checkout-table th {\n text-align: left;\n padding: 15px 0px;\n border-bottom: 1px solid #aaa;\n}\n.checkout-table td {\n padding: 8px 0px;\n}\n.checkout-button {\n float: right;\n width: 120px;\n height: 40px;\n margin-top: 20px;\n}\n.total td {\n border-top: 1px solid #aaa;\n padding-top: 10px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/pages/CartPage.vue","\n.product {\n padding: 10px 0px;\n border-bottom: 1px solid #eee;\n width: 400px;\n}\n.title {\n color: #312377;\n}\n.price {\n float: right;\n}\n.product-container {\n margin-bottom: 50px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/pages/HomePage.vue","\n.product-item {\n margin: 10px 10px;\n width: 500px;\n height: 400px;\n border-bottom: 1px solid #aaa;\n}\n.back-link {\n font-size: 20px;\n}\n.product-title {\n padding-top: 120px;\n text-align: center;\n margin: 0 auto;\n font-size: 26px;\n}\n.product-details {\n margin-top: 120px;\n}\n.inventory {\n float: left;\n font-size: 20px;\n margin-top: 15px;\n}\n.add-button {\n float: right;\n width: 140px;\n height: 50px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/pages/ProductPage.vue"],"sourceRoot":""} -------------------------------------------------------------------------------- /example/spa/static/js/app.8d8d5cefbdfeb5ce0c88.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([1],{21:function(t,e,n){"use strict";var r=[{id:1,title:"iPad 4 Mini",price:500.01,inventory:2},{id:2,title:"H&M T-Shirt White",price:10.99,inventory:10},{id:3,title:"Charli XCX - Sucker CD",price:19.99,inventory:5}];e.a={getProducts:function(t){setTimeout(function(){return t(r)},100)},buyProducts:function(t,e,n){setTimeout(function(){Math.random()>.5||navigator.userAgent.indexOf("PhantomJS")>-1?e():n()},100)}}},33:function(t,e,n){"use strict";var r=n(11),c=n(91),a=n(83),u=n.n(a),o=n(84),s=n.n(o),i=n(85),d=n.n(i);r.a.use(c.a),e.a=new c.a({routes:[{path:"/",name:"home",component:s.a},{path:"/cart",name:"cart",component:u.a},{path:"/product/:id",name:"product",component:d.a}]})},34:function(t,e,n){"use strict";var r=n(3),c=n(11),a=n(38),u=n(39);c.a.use(r.c),e.a=new r.c.Store({modules:{cart:a.a,products:u.a}})},35:function(t,e,n){n(77);var r=n(2)(n(40),n(86),null,null);t.exports=r.exports},37:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(11),c=n(35),a=n.n(c),u=n(33),o=n(34),s=n(36);n.n(s);n.i(s.sync)(o.a,u.a),r.a.config.productionTip=!1,new r.a({el:"#app",router:u.a,store:o.a,template:"",components:{App:a.a}})},38:function(t,e,n){"use strict";var r=n(47),c=n.n(r),a=n(21),u={added:[],lastCheckout:null},o={checkout:function(t,e){var n=t.commit,r=t.state,u=[].concat(c()(r.added));n("checkout_request"),a.a.buyProducts(e,function(){return n("checkout_successful")},function(){return n("checkout_failure",u)})}},s={add_to_cart:function(t,e){t.lastCheckout=null;var n=t.added.find(function(t){return t.id===e});n?n.quantity++:t.added.push({id:e,quantity:1})},checkout_request:function(t){t.added=[],t.lastCheckout=null},checkout_successful:function(t){t.lastCheckout="successful"},checkout_failure:function(t,e){t.added=e,t.lastCheckout="failed"}},i={cartProducts:function(t,e,n){return t.added.map(function(t){var e=t.id,r=t.quantity,c=n.products.all.find(function(t){return t.id===e});return{title:c.title,price:c.price,id:e,quantity:r}})},cartCount:function(t){var e=0;return t.added.forEach(function(t){var n=t.quantity;e+=n}),e}};e.a={state:u,actions:o,mutations:s,getters:i}},39:function(t,e,n){"use strict";var r=n(21),c={all:[]},a={addToCart:function(t,e){(0,t.commit)("add_to_cart",e.id)},getAllProducts:function(t){var e=t.commit;r.a.getProducts(function(t){e("recieve_products",t)})}},u={recieve_products:function(t,e){t.all=e},add_to_cart:function(t,e){t.all.find(function(t){return t.id===e}).inventory--}},o={allProducts:function(t){return t.all}};e.a={state:c,actions:a,mutations:u,getters:o}},40:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(82),c=n.n(r);e.default={components:{HeaderBar:c.a},name:"app"}},41:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(4),c=n.n(r),a=n(3);e.default={computed:c()({},n.i(a.a)(["cartCount"]))}},42:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(4),c=n.n(r),a=n(3);e.default={computed:c()({},n.i(a.a)({products:"cartProducts"}),{checkoutStatus:function(){return this.$store.state.cart.lastCheckout},total:function(){return this.products.reduce(function(t,e){return t+e.price*e.quantity},0)}}),methods:c()({},n.i(a.b)(["checkout"]))}},43:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(4),c=n.n(r),a=n(3);e.default={mounted:function(){this.getAllProducts()},computed:c()({},n.i(a.a)(["allProducts"])),methods:c()({},n.i(a.b)(["getAllProducts"]))}},44:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(4),c=n.n(r),a=n(3);e.default={mounted:function(){this.getAllProducts()},computed:c()({},n.i(a.a)(["allProducts"]),{product:function(){var t=parseInt(this.$route.params.id);return this.allProducts.find(function(e){return e.id===t})||{}}}),methods:c()({},n.i(a.b)(["getAllProducts","addToCart"]))}},77:function(t,e){},78:function(t,e){},79:function(t,e){},80:function(t,e){},81:function(t,e){},82:function(t,e,n){n(81);var r=n(2)(n(41),n(90),null,null);t.exports=r.exports},83:function(t,e,n){n(78);var r=n(2)(n(42),n(87),null,null);t.exports=r.exports},84:function(t,e,n){n(79);var r=n(2)(n(43),n(88),null,null);t.exports=r.exports},85:function(t,e,n){n(80);var r=n(2)(n(44),n(89),null,null);t.exports=r.exports},86:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"app"},[n("div",{staticClass:"header"},[n("header-bar")],1),t._v(" "),n("div",{staticClass:"page"},[n("router-view"),t._v(" "),t._m(0)],1)])},staticRenderFns:[function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"help-text"},[n("p",[t._v("Vue.js 2 Single Page App (SPA) Example with vuex and vue-router.")]),t._v(" "),n("a",{attrs:{href:"https://github.com/skyronic/vue-spa"}},[t._v("View Source Code")])])}]}},87:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"cart"},[n("p",{directives:[{name:"show",rawName:"v-show",value:!t.products.length,expression:"!products.length"}]},[n("i",[t._v("Please add some products to cart.")])]),t._v(" "),n("div",{directives:[{name:"show",rawName:"v-show",value:t.products.length>0,expression:"products.length > 0"}]},[n("table",{staticClass:"checkout-table"},[t._m(0),t._v(" "),n("tbody",[t._l(t.products,function(e){return n("tr",[n("td",[n("router-link",{attrs:{to:{name:"product",params:{id:e.id}}}},[t._v(t._s(e.title))])],1),t._v(" "),n("td",[t._v("$ "+t._s(e.price))]),t._v(" "),n("td",[t._v(t._s(e.quantity))]),t._v(" "),n("td",[t._v("$ "+t._s(e.price*e.quantity))])])}),t._v(" "),n("tr",{staticClass:"total"},[t._m(1),t._v(" "),n("td"),t._v(" "),n("td"),t._v(" "),n("td",[t._v("$ "+t._s(t.total))])])],2)]),t._v(" "),n("p",[n("button",{staticClass:"checkout-button",attrs:{disabled:!t.products.length},on:{click:function(e){t.checkout(t.products)}}},[t._v("Checkout")])])]),t._v(" "),n("p",{directives:[{name:"show",rawName:"v-show",value:t.checkoutStatus,expression:"checkoutStatus"}]},[t._v("Checkout "+t._s(t.checkoutStatus)+".")])])},staticRenderFns:[function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("thead",[n("tr",[n("th",[t._v("Name")]),t._v(" "),n("th",[t._v("Quantity")]),t._v(" "),n("th",[t._v("Per Unit")]),t._v(" "),n("th",[t._v("Total")])])])},function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("td",[n("b",[t._v("TOTAL")])])}]}},88:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"product-container"},t._l(t.allProducts,function(e){return n("div",{staticClass:"product"},[n("router-link",{staticClass:"title",attrs:{to:{name:"product",params:{id:e.id}}}},[t._v(t._s(e.title))]),t._v(" "),n("span",{staticClass:"price"},[t._v("$ "+t._s(e.price))])],1)}))},staticRenderFns:[]}},89:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"product-item"},[n("router-link",{staticClass:"back-link",attrs:{to:"/"}},[t._v("BACK")]),t._v(" "),n("div",{staticClass:"product-title"},[t._v(t._s(t.product.title))]),t._v(" "),n("div",{staticClass:"product-details"},[n("div",{staticClass:"inventory"},[t._v("In Stock: "+t._s(t.product.inventory))]),t._v(" "),n("button",{staticClass:"add-button",attrs:{disabled:!t.product.inventory},on:{click:function(e){t.addToCart(t.product)}}},[t._v(t._s(t.product.inventory>0?"Add to cart":"Out Of Stock"))])])],1)},staticRenderFns:[]}},90:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"menu-links"},[n("router-link",{attrs:{to:"/"}},[t._v("Home")]),t._v(" "),n("router-link",{attrs:{to:"/cart"}},[t._v("Cart ("+t._s(t.cartCount)+")")])],1)},staticRenderFns:[]}}},[37]); 2 | //# sourceMappingURL=app.8d8d5cefbdfeb5ce0c88.js.map -------------------------------------------------------------------------------- /example/spa/static/js/app.8d8d5cefbdfeb5ce0c88.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///static/js/app.8d8d5cefbdfeb5ce0c88.js","webpack:///./src/api/shop.js","webpack:///./src/router/index.js","webpack:///./src/store/index.js","webpack:///./src/App.vue?fc6a","webpack:///./src/main.js","webpack:///./src/store/cart.js","webpack:///./src/store/products.js","webpack:///App.vue","webpack:///HeaderBar.vue","webpack:///CartPage.vue","webpack:///HomePage.vue","webpack:///ProductPage.vue","webpack:///./src/components/HeaderBar.vue?2807","webpack:///./src/pages/CartPage.vue?c94b","webpack:///./src/pages/HomePage.vue?5557","webpack:///./src/pages/ProductPage.vue?648e","webpack:///./src/App.vue?7a8f","webpack:///./src/pages/CartPage.vue?0801","webpack:///./src/pages/HomePage.vue?b8b3","webpack:///./src/pages/ProductPage.vue?aa3c","webpack:///./src/components/HeaderBar.vue?857a"],"names":["webpackJsonp","21","module","__webpack_exports__","__webpack_require__","_products","id","title","price","inventory","getProducts","cb","setTimeout","buyProducts","products","errorCb","Math","random","navigator","userAgent","indexOf","33","__WEBPACK_IMPORTED_MODULE_0_vue__","__WEBPACK_IMPORTED_MODULE_1_vue_router__","__WEBPACK_IMPORTED_MODULE_2__pages_CartPage__","__WEBPACK_IMPORTED_MODULE_2__pages_CartPage___default","n","__WEBPACK_IMPORTED_MODULE_3__pages_HomePage__","__WEBPACK_IMPORTED_MODULE_3__pages_HomePage___default","__WEBPACK_IMPORTED_MODULE_4__pages_ProductPage__","__WEBPACK_IMPORTED_MODULE_4__pages_ProductPage___default","use","routes","path","name","component","a","34","__WEBPACK_IMPORTED_MODULE_0_vuex__","__WEBPACK_IMPORTED_MODULE_1_vue__","__WEBPACK_IMPORTED_MODULE_2__cart__","__WEBPACK_IMPORTED_MODULE_3__products__","Store","modules","cart","35","exports","Component","37","Object","defineProperty","value","__WEBPACK_IMPORTED_MODULE_1__App__","__WEBPACK_IMPORTED_MODULE_1__App___default","__WEBPACK_IMPORTED_MODULE_2__router__","__WEBPACK_IMPORTED_MODULE_3__store_index__","__WEBPACK_IMPORTED_MODULE_4_vuex_router_sync__","i","config","productionTip","el","router","store","template","components","App","38","__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray__","__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray___default","__WEBPACK_IMPORTED_MODULE_1__api_shop__","state","added","lastCheckout","actions","checkout","_ref","commit","savedCartItems","mutations","add_to_cart","productId","record","find","p","quantity","push","checkout_request","checkout_successful","checkout_failure","getters","cartProducts","rootState","map","_ref2","product","all","cartCount","totalCount","forEach","_ref3","39","__WEBPACK_IMPORTED_MODULE_0__api_shop__","addToCart","getAllProducts","recieve_products","allProducts","40","__WEBPACK_IMPORTED_MODULE_0__components_HeaderBar__","__WEBPACK_IMPORTED_MODULE_0__components_HeaderBar___default","HeaderBar","41","__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__","__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default","__WEBPACK_IMPORTED_MODULE_1_vuex__","computed","42","checkoutStatus","this","$store","total","reduce","methods","43","mounted","44","parseInt","$route","params","77","78","79","80","81","82","83","84","85","86","render","_vm","_h","$createElement","_c","_self","staticClass","_v","_m","staticRenderFns","attrs","href","87","directives","rawName","length","expression","_l","to","_s","disabled","on","click","$event","88","89","90"],"mappings":"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","file":"static/js/app.8d8d5cefbdfeb5ce0c88.js","sourcesContent":["webpackJsonp([1],{\n\n/***/ 21:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nvar _products = [{ 'id': 1, 'title': 'iPad 4 Mini', 'price': 500.01, 'inventory': 2 }, { 'id': 2, 'title': 'H&M T-Shirt White', 'price': 10.99, 'inventory': 10 }, { 'id': 3, 'title': 'Charli XCX - Sucker CD', 'price': 19.99, 'inventory': 5 }];\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n getProducts: function getProducts(cb) {\n setTimeout(function () {\n return cb(_products);\n }, 100);\n },\n buyProducts: function buyProducts(products, cb, errorCb) {\n setTimeout(function () {\n Math.random() > 0.5 || navigator.userAgent.indexOf('PhantomJS') > -1 ? cb() : errorCb();\n }, 100);\n }\n});\n\n/***/ }),\n\n/***/ 33:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_vue__ = __webpack_require__(11);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_router__ = __webpack_require__(91);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__pages_CartPage__ = __webpack_require__(83);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__pages_CartPage___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2__pages_CartPage__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__pages_HomePage__ = __webpack_require__(84);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__pages_HomePage___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_3__pages_HomePage__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__pages_ProductPage__ = __webpack_require__(85);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__pages_ProductPage___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4__pages_ProductPage__);\n\n\n\n\n\n\n__WEBPACK_IMPORTED_MODULE_0_vue__[\"a\" /* default */].use(__WEBPACK_IMPORTED_MODULE_1_vue_router__[\"a\" /* default */]);\n\n/* harmony default export */ __webpack_exports__[\"a\"] = (new __WEBPACK_IMPORTED_MODULE_1_vue_router__[\"a\" /* default */]({\n routes: [{\n path: '/',\n name: 'home',\n component: __WEBPACK_IMPORTED_MODULE_3__pages_HomePage___default.a\n }, {\n path: '/cart',\n name: 'cart',\n component: __WEBPACK_IMPORTED_MODULE_2__pages_CartPage___default.a\n }, {\n path: '/product/:id',\n name: 'product',\n component: __WEBPACK_IMPORTED_MODULE_4__pages_ProductPage___default.a\n }]\n}));\n\n/***/ }),\n\n/***/ 34:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_vuex__ = __webpack_require__(3);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue__ = __webpack_require__(11);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__cart__ = __webpack_require__(38);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__products__ = __webpack_require__(39);\n\n\n\n\n\n__WEBPACK_IMPORTED_MODULE_1_vue__[\"a\" /* default */].use(__WEBPACK_IMPORTED_MODULE_0_vuex__[\"c\" /* default */]);\n\n/* harmony default export */ __webpack_exports__[\"a\"] = (new __WEBPACK_IMPORTED_MODULE_0_vuex__[\"c\" /* default */].Store({\n modules: {\n cart: __WEBPACK_IMPORTED_MODULE_2__cart__[\"a\" /* default */],\n products: __WEBPACK_IMPORTED_MODULE_3__products__[\"a\" /* default */]\n }\n}));\n\n/***/ }),\n\n/***/ 35:\n/***/ (function(module, exports, __webpack_require__) {\n\n\n/* styles */\n__webpack_require__(77)\n\nvar Component = __webpack_require__(2)(\n /* script */\n __webpack_require__(40),\n /* template */\n __webpack_require__(86),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n/***/ }),\n\n/***/ 37:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_vue__ = __webpack_require__(11);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__App__ = __webpack_require__(35);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__App___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1__App__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__router__ = __webpack_require__(33);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__store_index__ = __webpack_require__(34);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4_vuex_router_sync__ = __webpack_require__(36);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4_vuex_router_sync___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_4_vuex_router_sync__);\n\n\n\n\n\n\n\n__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_4_vuex_router_sync__[\"sync\"])(__WEBPACK_IMPORTED_MODULE_3__store_index__[\"a\" /* default */], __WEBPACK_IMPORTED_MODULE_2__router__[\"a\" /* default */]);\n\n__WEBPACK_IMPORTED_MODULE_0_vue__[\"a\" /* default */].config.productionTip = false;\n\nnew __WEBPACK_IMPORTED_MODULE_0_vue__[\"a\" /* default */]({\n el: '#app',\n router: __WEBPACK_IMPORTED_MODULE_2__router__[\"a\" /* default */],\n store: __WEBPACK_IMPORTED_MODULE_3__store_index__[\"a\" /* default */],\n template: '',\n components: { App: __WEBPACK_IMPORTED_MODULE_1__App___default.a }\n});\n\n/***/ }),\n\n/***/ 38:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray__ = __webpack_require__(47);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__api_shop__ = __webpack_require__(21);\n\n\n\nvar state = {\n added: [],\n lastCheckout: null\n};\n\nvar actions = {\n checkout: function checkout(_ref, products) {\n var commit = _ref.commit,\n state = _ref.state;\n\n var savedCartItems = [].concat(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_toConsumableArray___default()(state.added));\n commit('checkout_request');\n __WEBPACK_IMPORTED_MODULE_1__api_shop__[\"a\" /* default */].buyProducts(products, function () {\n return commit('checkout_successful');\n }, function () {\n return commit('checkout_failure', savedCartItems);\n });\n }\n};\n\nvar mutations = {\n add_to_cart: function add_to_cart(state, productId) {\n state.lastCheckout = null;\n var record = state.added.find(function (p) {\n return p.id === productId;\n });\n if (!record) {\n state.added.push({\n id: productId,\n quantity: 1\n });\n } else {\n record.quantity++;\n }\n },\n checkout_request: function checkout_request(state) {\n state.added = [];\n state.lastCheckout = null;\n },\n checkout_successful: function checkout_successful(state) {\n state.lastCheckout = 'successful';\n },\n checkout_failure: function checkout_failure(state, savedCartItems) {\n state.added = savedCartItems;\n state.lastCheckout = 'failed';\n }\n};\n\nvar getters = {\n cartProducts: function cartProducts(state, getters, rootState) {\n return state.added.map(function (_ref2) {\n var id = _ref2.id,\n quantity = _ref2.quantity;\n\n var product = rootState.products.all.find(function (p) {\n return p.id === id;\n });\n return {\n title: product.title,\n price: product.price,\n id: id,\n quantity: quantity\n };\n });\n },\n cartCount: function cartCount(state) {\n var totalCount = 0;\n state.added.forEach(function (_ref3) {\n var quantity = _ref3.quantity;\n\n totalCount += quantity;\n });\n return totalCount;\n }\n};\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n state: state,\n actions: actions,\n mutations: mutations,\n getters: getters\n});\n\n/***/ }),\n\n/***/ 39:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__api_shop__ = __webpack_require__(21);\n\n\nvar state = {\n all: []\n};\n\nvar actions = {\n addToCart: function addToCart(_ref, product) {\n var commit = _ref.commit;\n\n commit('add_to_cart', product.id);\n },\n getAllProducts: function getAllProducts(_ref2) {\n var commit = _ref2.commit;\n\n __WEBPACK_IMPORTED_MODULE_0__api_shop__[\"a\" /* default */].getProducts(function (products) {\n commit('recieve_products', products);\n });\n }\n};\n\nvar mutations = {\n recieve_products: function recieve_products(state, products) {\n state.all = products;\n },\n add_to_cart: function add_to_cart(state, productId) {\n state.all.find(function (p) {\n return p.id === productId;\n }).inventory--;\n }\n};\n\nvar getters = {\n allProducts: function allProducts(state) {\n return state.all;\n }\n};\n\n/* harmony default export */ __webpack_exports__[\"a\"] = ({\n state: state,\n actions: actions,\n mutations: mutations,\n getters: getters\n});\n\n/***/ }),\n\n/***/ 40:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__components_HeaderBar__ = __webpack_require__(82);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__components_HeaderBar___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__components_HeaderBar__);\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n components: {\n HeaderBar: __WEBPACK_IMPORTED_MODULE_0__components_HeaderBar___default.a\n },\n name: 'app'\n});\n\n/***/ }),\n\n/***/ 41:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__ = __webpack_require__(4);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vuex__ = __webpack_require__(3);\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n computed: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"a\" /* mapGetters */])(['cartCount']))\n});\n\n/***/ }),\n\n/***/ 42:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__ = __webpack_require__(4);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vuex__ = __webpack_require__(3);\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n computed: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"a\" /* mapGetters */])({\n products: 'cartProducts'\n }), {\n checkoutStatus: function checkoutStatus() {\n return this.$store.state.cart.lastCheckout;\n },\n total: function total() {\n return this.products.reduce(function (total, p) {\n return total + p.price * p.quantity;\n }, 0);\n }\n }),\n methods: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"b\" /* mapActions */])(['checkout']))\n});\n\n/***/ }),\n\n/***/ 43:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__ = __webpack_require__(4);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vuex__ = __webpack_require__(3);\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n mounted: function mounted() {\n this.getAllProducts();\n },\n\n computed: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"a\" /* mapGetters */])(['allProducts'])),\n methods: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"b\" /* mapActions */])(['getAllProducts']))\n});\n\n/***/ }),\n\n/***/ 44:\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\nObject.defineProperty(__webpack_exports__, \"__esModule\", { value: true });\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__ = __webpack_require__(4);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends__);\n/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vuex__ = __webpack_require__(3);\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n mounted: function mounted() {\n this.getAllProducts();\n },\n\n computed: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"a\" /* mapGetters */])(['allProducts']), {\n product: function product() {\n var id = parseInt(this.$route.params.id);\n return this.allProducts.find(function (p) {\n return p.id === id;\n }) || {};\n }\n }),\n methods: __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_extends___default()({}, __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1_vuex__[\"b\" /* mapActions */])(['getAllProducts', 'addToCart']))\n});\n\n/***/ }),\n\n/***/ 77:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n\n/***/ 78:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n\n/***/ 79:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n\n/***/ 80:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n\n/***/ 81:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n\n/***/ 82:\n/***/ (function(module, exports, __webpack_require__) {\n\n\n/* styles */\n__webpack_require__(81)\n\nvar Component = __webpack_require__(2)(\n /* script */\n __webpack_require__(41),\n /* template */\n __webpack_require__(90),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n/***/ }),\n\n/***/ 83:\n/***/ (function(module, exports, __webpack_require__) {\n\n\n/* styles */\n__webpack_require__(78)\n\nvar Component = __webpack_require__(2)(\n /* script */\n __webpack_require__(42),\n /* template */\n __webpack_require__(87),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n/***/ }),\n\n/***/ 84:\n/***/ (function(module, exports, __webpack_require__) {\n\n\n/* styles */\n__webpack_require__(79)\n\nvar Component = __webpack_require__(2)(\n /* script */\n __webpack_require__(43),\n /* template */\n __webpack_require__(88),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n/***/ }),\n\n/***/ 85:\n/***/ (function(module, exports, __webpack_require__) {\n\n\n/* styles */\n__webpack_require__(80)\n\nvar Component = __webpack_require__(2)(\n /* script */\n __webpack_require__(44),\n /* template */\n __webpack_require__(89),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n/***/ }),\n\n/***/ 86:\n/***/ (function(module, exports) {\n\nmodule.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"app\"\n }, [_c('div', {\n staticClass: \"header\"\n }, [_c('header-bar')], 1), _vm._v(\" \"), _c('div', {\n staticClass: \"page\"\n }, [_c('router-view'), _vm._v(\" \"), _vm._m(0)], 1)])\n},staticRenderFns: [function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"help-text\"\n }, [_c('p', [_vm._v(\"Vue.js 2 Single Page App (SPA) Example with vuex and vue-router.\")]), _vm._v(\" \"), _c('a', {\n attrs: {\n \"href\": \"https://github.com/skyronic/vue-spa\"\n }\n }, [_vm._v(\"View Source Code\")])])\n}]}\n\n/***/ }),\n\n/***/ 87:\n/***/ (function(module, exports) {\n\nmodule.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"cart\"\n }, [_c('p', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (!_vm.products.length),\n expression: \"!products.length\"\n }]\n }, [_c('i', [_vm._v(\"Please add some products to cart.\")])]), _vm._v(\" \"), _c('div', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (_vm.products.length > 0),\n expression: \"products.length > 0\"\n }]\n }, [_c('table', {\n staticClass: \"checkout-table\"\n }, [_vm._m(0), _vm._v(\" \"), _c('tbody', [_vm._l((_vm.products), function(p) {\n return _c('tr', [_c('td', [_c('router-link', {\n attrs: {\n \"to\": {\n name: 'product',\n params: {\n id: p.id\n }\n }\n }\n }, [_vm._v(_vm._s(p.title))])], 1), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(p.price))]), _vm._v(\" \"), _c('td', [_vm._v(_vm._s(p.quantity))]), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(p.price * p.quantity))])])\n }), _vm._v(\" \"), _c('tr', {\n staticClass: \"total\"\n }, [_vm._m(1), _vm._v(\" \"), _c('td'), _vm._v(\" \"), _c('td'), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(_vm.total))])])], 2)]), _vm._v(\" \"), _c('p', [_c('button', {\n staticClass: \"checkout-button\",\n attrs: {\n \"disabled\": !_vm.products.length\n },\n on: {\n \"click\": function($event) {\n _vm.checkout(_vm.products)\n }\n }\n }, [_vm._v(\"Checkout\")])])]), _vm._v(\" \"), _c('p', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (_vm.checkoutStatus),\n expression: \"checkoutStatus\"\n }]\n }, [_vm._v(\"Checkout \" + _vm._s(_vm.checkoutStatus) + \".\")])])\n},staticRenderFns: [function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('thead', [_c('tr', [_c('th', [_vm._v(\"Name\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Quantity\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Per Unit\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Total\")])])])\n},function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('td', [_c('b', [_vm._v(\"TOTAL\")])])\n}]}\n\n/***/ }),\n\n/***/ 88:\n/***/ (function(module, exports) {\n\nmodule.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"product-container\"\n }, _vm._l((_vm.allProducts), function(p) {\n return _c('div', {\n staticClass: \"product\"\n }, [_c('router-link', {\n staticClass: \"title\",\n attrs: {\n \"to\": {\n name: 'product',\n params: {\n id: p.id\n }\n }\n }\n }, [_vm._v(_vm._s(p.title))]), _vm._v(\" \"), _c('span', {\n staticClass: \"price\"\n }, [_vm._v(\"$ \" + _vm._s(p.price))])], 1)\n }))\n},staticRenderFns: []}\n\n/***/ }),\n\n/***/ 89:\n/***/ (function(module, exports) {\n\nmodule.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"product-item\"\n }, [_c('router-link', {\n staticClass: \"back-link\",\n attrs: {\n \"to\": \"/\"\n }\n }, [_vm._v(\"BACK\")]), _vm._v(\" \"), _c('div', {\n staticClass: \"product-title\"\n }, [_vm._v(_vm._s(_vm.product.title))]), _vm._v(\" \"), _c('div', {\n staticClass: \"product-details\"\n }, [_c('div', {\n staticClass: \"inventory\"\n }, [_vm._v(\"In Stock: \" + _vm._s(_vm.product.inventory))]), _vm._v(\" \"), _c('button', {\n staticClass: \"add-button\",\n attrs: {\n \"disabled\": !_vm.product.inventory\n },\n on: {\n \"click\": function($event) {\n _vm.addToCart(_vm.product)\n }\n }\n }, [_vm._v(_vm._s(_vm.product.inventory > 0 ? \"Add to cart\" : \"Out Of Stock\"))])])], 1)\n},staticRenderFns: []}\n\n/***/ }),\n\n/***/ 90:\n/***/ (function(module, exports) {\n\nmodule.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"menu-links\"\n }, [_c('router-link', {\n attrs: {\n \"to\": \"/\"\n }\n }, [_vm._v(\"Home\")]), _vm._v(\" \"), _c('router-link', {\n attrs: {\n \"to\": \"/cart\"\n }\n }, [_vm._v(\"Cart (\" + _vm._s(_vm.cartCount) + \")\")])], 1)\n},staticRenderFns: []}\n\n/***/ })\n\n},[37]);\n\n\n// WEBPACK FOOTER //\n// static/js/app.8d8d5cefbdfeb5ce0c88.js","const _products = [\n {'id': 1, 'title': 'iPad 4 Mini', 'price': 500.01, 'inventory': 2},\n {'id': 2, 'title': 'H&M T-Shirt White', 'price': 10.99, 'inventory': 10},\n {'id': 3, 'title': 'Charli XCX - Sucker CD', 'price': 19.99, 'inventory': 5}\n]\n\nexport default {\n getProducts (cb) {\n setTimeout(() => cb(_products), 100)\n },\n\n buyProducts (products, cb, errorCb) {\n setTimeout(() => {\n // simulate random checkout failure.\n (Math.random() > 0.5 || navigator.userAgent.indexOf('PhantomJS') > -1)\n ? cb()\n : errorCb()\n }, 100)\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/api/shop.js","import Vue from 'vue'\nimport Router from 'vue-router'\nimport CartPage from '@/pages/CartPage'\nimport HomePage from '@/pages/HomePage'\nimport ProductPage from '@/pages/ProductPage'\n\nVue.use(Router)\n\nexport default new Router({\n routes: [\n {\n path: '/',\n name: 'home',\n component: HomePage\n },\n {\n path: '/cart',\n name: 'cart',\n component: CartPage\n },\n {\n path: '/product/:id',\n name: 'product',\n component: ProductPage\n }\n ]\n})\n\n\n\n// WEBPACK FOOTER //\n// ./src/router/index.js","import Vuex from 'vuex'\nimport Vue from 'vue'\nimport cart from './cart'\nimport products from './products'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n modules: {\n cart,\n products\n }\n})\n\n\n\n// WEBPACK FOOTER //\n// ./src/store/index.js","\n/* styles */\nrequire(\"!!../node_modules/extract-text-webpack-plugin/loader.js?{\\\"omit\\\":1,\\\"remove\\\":true}!vue-style-loader!css-loader?{\\\"minimize\\\":true,\\\"sourceMap\\\":true}!../node_modules/vue-loader/lib/style-compiler/index?{\\\"id\\\":\\\"data-v-3850f906\\\",\\\"scoped\\\":false,\\\"hasInlineConfig\\\":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue\")\n\nvar Component = require(\"!../node_modules/vue-loader/lib/component-normalizer\")(\n /* script */\n require(\"!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue\"),\n /* template */\n require(\"!!../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-3850f906\\\"}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue\"),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/App.vue\n// module id = 35\n// module chunks = 1","// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base.conf with an alias.\nimport Vue from 'vue'\nimport App from './App'\nimport router from './router'\nimport store from './store/index'\nimport { sync } from 'vuex-router-sync'\n\nsync(store, router)\n\nVue.config.productionTip = false\n\n/* eslint-disable no-new */\nnew Vue({\n el: '#app',\n router,\n store,\n template: '',\n components: { App }\n})\n\n\n\n// WEBPACK FOOTER //\n// ./src/main.js","import shop from '@/api/shop'\n\nconst state = {\n added: [],\n lastCheckout: null\n}\n\nconst actions = {\n // The first argument is the vuex store, but we're using only the\n // dispatch function, which applies a mutation to the store,\n // and the current state of the store\n checkout ({commit, state}, products) {\n const savedCartItems = [...state.added]\n commit('checkout_request')\n shop.buyProducts(\n products,\n () => commit('checkout_successful'),\n () => commit('checkout_failure', savedCartItems)\n )\n }\n}\n\nconst mutations = {\n add_to_cart (state, productId) {\n state.lastCheckout = null\n const record = state.added.find(p => p.id === productId)\n if (!record) {\n state.added.push({\n id: productId,\n quantity: 1\n })\n } else {\n record.quantity++\n }\n },\n checkout_request (state) {\n // clear cart\n state.added = []\n state.lastCheckout = null\n },\n checkout_successful (state) {\n state.lastCheckout = 'successful'\n },\n checkout_failure (state, savedCartItems) {\n // rollback to the cart saved before sending the request\n state.added = savedCartItems\n state.lastCheckout = 'failed'\n }\n}\n\nconst getters = {\n cartProducts (state, getters, rootState) {\n return state.added.map(({ id, quantity }) => {\n const product = rootState.products.all.find(p => p.id === id)\n return {\n title: product.title,\n price: product.price,\n id,\n quantity\n }\n })\n },\n cartCount (state) {\n var totalCount = 0\n state.added.forEach(({ quantity }) => {\n totalCount += quantity\n })\n return totalCount\n }\n}\n\nexport default {\n state,\n actions,\n mutations,\n getters\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/store/cart.js","import shop from '@/api/shop'\n\nconst state = {\n all: []\n}\n\nconst actions = {\n addToCart ({commit}, product) {\n commit('add_to_cart', product.id)\n },\n getAllProducts ({commit}) {\n shop.getProducts(products => {\n commit('recieve_products', products)\n })\n }\n}\n\nconst mutations = {\n recieve_products (state, products) {\n state.all = products\n },\n add_to_cart (state, productId) {\n state.all.find(p => p.id === productId).inventory--\n }\n}\n\nconst getters = {\n allProducts (state) {\n return state.all\n }\n}\n\nexport default {\n state,\n actions,\n mutations,\n getters\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/store/products.js","\n\n\n\n\n\n\n\n// WEBPACK FOOTER //\n// App.vue?4c271d8e","\n\n\n\n\n\n\n\n// WEBPACK FOOTER //\n// HeaderBar.vue?2f1155b0","\n\n\n\n\n\n\n\n// WEBPACK FOOTER //\n// CartPage.vue?4fe392d7","\n\n\n\n\n\n\n\n// WEBPACK FOOTER //\n// HomePage.vue?1e1f477f","\n\n\n\n\n\n\n\n// WEBPACK FOOTER //\n// ProductPage.vue?ca531178","\n/* styles */\nrequire(\"!!../../node_modules/extract-text-webpack-plugin/loader.js?{\\\"omit\\\":1,\\\"remove\\\":true}!vue-style-loader!css-loader?{\\\"minimize\\\":true,\\\"sourceMap\\\":true}!../../node_modules/vue-loader/lib/style-compiler/index?{\\\"id\\\":\\\"data-v-ddfb4590\\\",\\\"scoped\\\":false,\\\"hasInlineConfig\\\":false}!../../node_modules/vue-loader/lib/selector?type=styles&index=0!./HeaderBar.vue\")\n\nvar Component = require(\"!../../node_modules/vue-loader/lib/component-normalizer\")(\n /* script */\n require(\"!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./HeaderBar.vue\"),\n /* template */\n require(\"!!../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-ddfb4590\\\"}!../../node_modules/vue-loader/lib/selector?type=template&index=0!./HeaderBar.vue\"),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/components/HeaderBar.vue\n// module id = 82\n// module chunks = 1","\n/* styles */\nrequire(\"!!../../node_modules/extract-text-webpack-plugin/loader.js?{\\\"omit\\\":1,\\\"remove\\\":true}!vue-style-loader!css-loader?{\\\"minimize\\\":true,\\\"sourceMap\\\":true}!../../node_modules/vue-loader/lib/style-compiler/index?{\\\"id\\\":\\\"data-v-3e309336\\\",\\\"scoped\\\":false,\\\"hasInlineConfig\\\":false}!../../node_modules/vue-loader/lib/selector?type=styles&index=0!./CartPage.vue\")\n\nvar Component = require(\"!../../node_modules/vue-loader/lib/component-normalizer\")(\n /* script */\n require(\"!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./CartPage.vue\"),\n /* template */\n require(\"!!../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-3e309336\\\"}!../../node_modules/vue-loader/lib/selector?type=template&index=0!./CartPage.vue\"),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/pages/CartPage.vue\n// module id = 83\n// module chunks = 1","\n/* styles */\nrequire(\"!!../../node_modules/extract-text-webpack-plugin/loader.js?{\\\"omit\\\":1,\\\"remove\\\":true}!vue-style-loader!css-loader?{\\\"minimize\\\":true,\\\"sourceMap\\\":true}!../../node_modules/vue-loader/lib/style-compiler/index?{\\\"id\\\":\\\"data-v-931ffdb8\\\",\\\"scoped\\\":false,\\\"hasInlineConfig\\\":false}!../../node_modules/vue-loader/lib/selector?type=styles&index=0!./HomePage.vue\")\n\nvar Component = require(\"!../../node_modules/vue-loader/lib/component-normalizer\")(\n /* script */\n require(\"!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./HomePage.vue\"),\n /* template */\n require(\"!!../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-931ffdb8\\\"}!../../node_modules/vue-loader/lib/selector?type=template&index=0!./HomePage.vue\"),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/pages/HomePage.vue\n// module id = 84\n// module chunks = 1","\n/* styles */\nrequire(\"!!../../node_modules/extract-text-webpack-plugin/loader.js?{\\\"omit\\\":1,\\\"remove\\\":true}!vue-style-loader!css-loader?{\\\"minimize\\\":true,\\\"sourceMap\\\":true}!../../node_modules/vue-loader/lib/style-compiler/index?{\\\"id\\\":\\\"data-v-c6682510\\\",\\\"scoped\\\":false,\\\"hasInlineConfig\\\":false}!../../node_modules/vue-loader/lib/selector?type=styles&index=0!./ProductPage.vue\")\n\nvar Component = require(\"!../../node_modules/vue-loader/lib/component-normalizer\")(\n /* script */\n require(\"!!babel-loader!../../node_modules/vue-loader/lib/selector?type=script&index=0!./ProductPage.vue\"),\n /* template */\n require(\"!!../../node_modules/vue-loader/lib/template-compiler/index?{\\\"id\\\":\\\"data-v-c6682510\\\"}!../../node_modules/vue-loader/lib/selector?type=template&index=0!./ProductPage.vue\"),\n /* scopeId */\n null,\n /* cssModules */\n null\n)\n\nmodule.exports = Component.exports\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/pages/ProductPage.vue\n// module id = 85\n// module chunks = 1","module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"app\"\n }, [_c('div', {\n staticClass: \"header\"\n }, [_c('header-bar')], 1), _vm._v(\" \"), _c('div', {\n staticClass: \"page\"\n }, [_c('router-view'), _vm._v(\" \"), _vm._m(0)], 1)])\n},staticRenderFns: [function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"help-text\"\n }, [_c('p', [_vm._v(\"Vue.js 2 Single Page App (SPA) Example with vuex and vue-router.\")]), _vm._v(\" \"), _c('a', {\n attrs: {\n \"href\": \"https://github.com/skyronic/vue-spa\"\n }\n }, [_vm._v(\"View Source Code\")])])\n}]}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/vue-loader/lib/template-compiler?{\"id\":\"data-v-3850f906\"}!./~/vue-loader/lib/selector.js?type=template&index=0!./src/App.vue\n// module id = 86\n// module chunks = 1","module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"cart\"\n }, [_c('p', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (!_vm.products.length),\n expression: \"!products.length\"\n }]\n }, [_c('i', [_vm._v(\"Please add some products to cart.\")])]), _vm._v(\" \"), _c('div', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (_vm.products.length > 0),\n expression: \"products.length > 0\"\n }]\n }, [_c('table', {\n staticClass: \"checkout-table\"\n }, [_vm._m(0), _vm._v(\" \"), _c('tbody', [_vm._l((_vm.products), function(p) {\n return _c('tr', [_c('td', [_c('router-link', {\n attrs: {\n \"to\": {\n name: 'product',\n params: {\n id: p.id\n }\n }\n }\n }, [_vm._v(_vm._s(p.title))])], 1), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(p.price))]), _vm._v(\" \"), _c('td', [_vm._v(_vm._s(p.quantity))]), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(p.price * p.quantity))])])\n }), _vm._v(\" \"), _c('tr', {\n staticClass: \"total\"\n }, [_vm._m(1), _vm._v(\" \"), _c('td'), _vm._v(\" \"), _c('td'), _vm._v(\" \"), _c('td', [_vm._v(\"$ \" + _vm._s(_vm.total))])])], 2)]), _vm._v(\" \"), _c('p', [_c('button', {\n staticClass: \"checkout-button\",\n attrs: {\n \"disabled\": !_vm.products.length\n },\n on: {\n \"click\": function($event) {\n _vm.checkout(_vm.products)\n }\n }\n }, [_vm._v(\"Checkout\")])])]), _vm._v(\" \"), _c('p', {\n directives: [{\n name: \"show\",\n rawName: \"v-show\",\n value: (_vm.checkoutStatus),\n expression: \"checkoutStatus\"\n }]\n }, [_vm._v(\"Checkout \" + _vm._s(_vm.checkoutStatus) + \".\")])])\n},staticRenderFns: [function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('thead', [_c('tr', [_c('th', [_vm._v(\"Name\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Quantity\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Per Unit\")]), _vm._v(\" \"), _c('th', [_vm._v(\"Total\")])])])\n},function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('td', [_c('b', [_vm._v(\"TOTAL\")])])\n}]}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/vue-loader/lib/template-compiler?{\"id\":\"data-v-3e309336\"}!./~/vue-loader/lib/selector.js?type=template&index=0!./src/pages/CartPage.vue\n// module id = 87\n// module chunks = 1","module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"product-container\"\n }, _vm._l((_vm.allProducts), function(p) {\n return _c('div', {\n staticClass: \"product\"\n }, [_c('router-link', {\n staticClass: \"title\",\n attrs: {\n \"to\": {\n name: 'product',\n params: {\n id: p.id\n }\n }\n }\n }, [_vm._v(_vm._s(p.title))]), _vm._v(\" \"), _c('span', {\n staticClass: \"price\"\n }, [_vm._v(\"$ \" + _vm._s(p.price))])], 1)\n }))\n},staticRenderFns: []}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/vue-loader/lib/template-compiler?{\"id\":\"data-v-931ffdb8\"}!./~/vue-loader/lib/selector.js?type=template&index=0!./src/pages/HomePage.vue\n// module id = 88\n// module chunks = 1","module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"product-item\"\n }, [_c('router-link', {\n staticClass: \"back-link\",\n attrs: {\n \"to\": \"/\"\n }\n }, [_vm._v(\"BACK\")]), _vm._v(\" \"), _c('div', {\n staticClass: \"product-title\"\n }, [_vm._v(_vm._s(_vm.product.title))]), _vm._v(\" \"), _c('div', {\n staticClass: \"product-details\"\n }, [_c('div', {\n staticClass: \"inventory\"\n }, [_vm._v(\"In Stock: \" + _vm._s(_vm.product.inventory))]), _vm._v(\" \"), _c('button', {\n staticClass: \"add-button\",\n attrs: {\n \"disabled\": !_vm.product.inventory\n },\n on: {\n \"click\": function($event) {\n _vm.addToCart(_vm.product)\n }\n }\n }, [_vm._v(_vm._s(_vm.product.inventory > 0 ? \"Add to cart\" : \"Out Of Stock\"))])])], 1)\n},staticRenderFns: []}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/vue-loader/lib/template-compiler?{\"id\":\"data-v-c6682510\"}!./~/vue-loader/lib/selector.js?type=template&index=0!./src/pages/ProductPage.vue\n// module id = 89\n// module chunks = 1","module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;\n return _c('div', {\n staticClass: \"menu-links\"\n }, [_c('router-link', {\n attrs: {\n \"to\": \"/\"\n }\n }, [_vm._v(\"Home\")]), _vm._v(\" \"), _c('router-link', {\n attrs: {\n \"to\": \"/cart\"\n }\n }, [_vm._v(\"Cart (\" + _vm._s(_vm.cartCount) + \")\")])], 1)\n},staticRenderFns: []}\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./~/vue-loader/lib/template-compiler?{\"id\":\"data-v-ddfb4590\"}!./~/vue-loader/lib/selector.js?type=template&index=0!./src/components/HeaderBar.vue\n// module id = 90\n// module chunks = 1"],"sourceRoot":""} -------------------------------------------------------------------------------- /example/spa/static/js/manifest.68b98c1982578ae4634f.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(t,c,i){for(var u,a,f,s=0,l=[];s