├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .env ├── .env.production ├── .eslintrc.js ├── .gitignore ├── .htpasswd ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── brotli.conf ├── cypress.json ├── docker-base └── Dockerfile.alpine ├── docker-compose.yml ├── jest.config.js ├── nginx-cache ├── Dockerfile ├── brotli.conf ├── cache.template ├── nginx.conf └── startup.sh ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── prod.template ├── public ├── favicon.ico └── index.html ├── site.conf ├── sonar-project.properties ├── src ├── App.vue ├── assets │ ├── MesloLGS-Regular.woff │ ├── MesloLGS-Regular.woff2 │ ├── OxygenMono-Regular.woff2 │ └── style.css ├── common │ └── config.js ├── components │ ├── About.vue │ ├── BaseFooter.vue │ ├── BaseFooterItem.vue │ ├── BaseMenu.vue │ ├── BaseMenuItem.vue │ ├── BaseTag.vue │ ├── Blog.vue │ ├── BookList.vue │ ├── BookListItem.vue │ ├── Contact.vue │ ├── Home.vue │ ├── MenuActions.vue │ ├── NavFooter.vue │ ├── Navigation.vue │ ├── NotFound.vue │ ├── Post.vue │ ├── PostEditor.vue │ ├── PostHeader.vue │ ├── PostList.vue │ ├── PostListItem.vue │ ├── PostListPage.vue │ ├── Project.vue │ ├── Projects.vue │ ├── SocialSharingList.vue │ ├── SubscribePage.vue │ └── TableOfContents.vue ├── main.js ├── router │ └── index.js └── store │ └── index.js ├── startup.sh ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ ├── .eslintrc.js │ ├── __snapshots__ │ ├── baseFooter.spec.js.snap │ ├── baseMenu.spec.js.snap │ ├── bookList.spec.js.snap │ ├── menuActions.spec.js.snap │ ├── post.spec.js.snap │ ├── postHeader.spec.js.snap │ ├── postList.spec.js.snap │ ├── project.spec.js.snap │ └── tableOfContents.spec.js.snap │ ├── baseFooter.spec.js │ ├── baseMenu.spec.js │ ├── baseTag.spec.js │ ├── bookList.spec.js │ ├── menuActions.spec.js │ ├── navigation.spec.js │ ├── post.spec.js │ ├── postHeader.spec.js │ ├── postList.spec.js │ ├── postListItem.spec.js │ ├── project.spec.js │ ├── socialSharingList.spec.js │ ├── store.spec.js │ └── tableOfContents.spec.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 4 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 160 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | POPULATE_DB=1 2 | PROD=1 3 | NGINX_HOST=localhost 4 | API_URL=localhost 5 | VUE_APP_API_URL=localhost 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=martinheinz.dev 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'max-len': ['error', { code: 180 }], 14 | 'brace-style': ["error", "stroustrup"], 15 | indent: ['error', 4], 16 | }, 17 | parserOptions: { 18 | parser: 'babel-eslint', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | **/dist 4 | 5 | **/tests/e2e/videos/ 6 | **/tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .key 27 | -------------------------------------------------------------------------------- /.htpasswd: -------------------------------------------------------------------------------- 1 | admin:$2y$11$SnpP7OAabk31WaS43tQRwefBj3uBu5K1bg3NeHW5ly8qARQdOb8Za 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | CC_TEST_REPORTER_ID=ff2fd3fd55146fc066871b6020f5e8e1103407d2821898c7f94fabd0d1075835 4 | 5 | matrix: 6 | include: 7 | - language: node_js 8 | node_js: 9 | - "node" 10 | script: 11 | - docker build -t blog-frontend . 12 | 13 | - language: node_js 14 | node_js: 15 | - "node" 16 | addons: 17 | sonarcloud: 18 | organization: martinheinz-github 19 | token: 20 | secure: "qglaIlxCS3KjHwaBzpL/rDG0/vwlDzH9NhAEfIUi9LdWn7zfq43UCLZfH35oQ5Xwj1E/lrw3aZ3CCXW4zzGQcK46FbaUk5ls2gKFjvkCMv0X6oMVUsFN1wjsRg43fJl5k3QD5OE8BN5F2jU6IC5fejAI8Pm19ytSC5VpsN86D/Ju7VddODEL2BgkBxeqGC37SbMhlhGBD5D8zw+FaSiy4bDaTbvS7VNruzvKi62f8n/geSTgbGwwtck2U5s7SR6DTz4oim/dbkx+b3PLNhe9+bmaZyFqy8wUPm0cduS6SI9VrWRxd9TbQkRXqmfhliDFv5NetH2oJahstsbGML5pvR1NxJmJjK+kjcInx0Cv2mbwkPWLM0UL6EKPg0+UNgTMXyAhftqpoF7CkFKPxSPWP0ES4CoUo9kLrUA+h/Vx72bF0czzeOrbnWQUJchHwqj+jiCLlVqE/Z6/dDt1O9tTgHdAGw6HOqXWOK6OmAjqXK3KOw+eWkHv/IzpEPNzYIrUgggwmEn8leGgezFGMOtqPzHuQefRimR0WqBE6qNrjGy8ASZ3ws1JxRWrMH3RXgg+G2nqptuAjEWLAXM+znZq4sTHDi1ToIhVAxXb3cg767gs+Rd6LM43QFmlDyuqAMS4VdNf983nHSbYAaDtpoEQRxV08MZ6DrqAV5/nJatXroI=" 21 | script: 22 | - npm run test:unit 23 | - sonar-scanner 24 | 25 | - language: node_js 26 | node_js: 27 | - "node" 28 | before_script: 29 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 30 | - chmod +x ./cc-test-reporter 31 | - ./cc-test-reporter before-build 32 | 33 | script: 34 | - npm run test:unit 35 | 36 | after_script: 37 | - ./cc-test-reporter after-build -t lcov ./coverage/lcov.info 38 | 39 | - language: node_js 40 | node_js: 41 | - "node" 42 | services: 43 | - docker 44 | if: branch = master 45 | script: 46 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 47 | - docker-compose build --no-cache 48 | - docker images 49 | - docker push ${DOCKER_USERNAME}/blog_frontend:latest 50 | 51 | notifications: 52 | email: false 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as builder 2 | COPY package*.json ./ 3 | RUN npm install --no-optional 4 | COPY . . 5 | RUN npm run build 6 | 7 | FROM martinheinz/nginx-brotli as runner 8 | COPY --from=builder dist /home/html 9 | COPY site.conf brotli.conf prod.template /etc/nginx/conf.d/ 10 | COPY nginx.conf .htpasswd /etc/nginx/ 11 | EXPOSE 8008 12 | COPY startup.sh /home/ 13 | RUN chmod 777 /home/startup.sh 14 | CMD ["sh", "/home/startup.sh"] 15 | 16 | # docker build -t martinheinz/blog_frontend . 17 | # docker run --name blog-frontend -p 8080:8080 --rm martinheinz/blog_frontend 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MartinHeinz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Frontend 2 | 3 | General Quality and Build: 4 | 5 | [![Build Status](https://travis-ci.com/MartinHeinz/blog-frontend.svg?branch=master)](https://travis-ci.com/MartinHeinz/blog-frontend) 6 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=MartinHeinz_blog-frontend&metric=coverage)](https://sonarcloud.io/dashboard?id=MartinHeinz_blog-frontend) 7 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=MartinHeinz_blog-frontend&metric=alert_status)](https://sonarcloud.io/dashboard?id=MartinHeinz_blog-frontend) 8 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=MartinHeinz_blog-frontend&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=MartinHeinz_blog-frontend) 9 | [![Maintainability](https://api.codeclimate.com/v1/badges/75a6e6323821fcece3ec/maintainability)](https://codeclimate.com/github/MartinHeinz/blog-frontend/maintainability) 10 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/728b4690245b4f768bd73773c06b735e)](https://app.codacy.com/app/MartinHeinz/blog-frontend?utm_source=github.com&utm_medium=referral&utm_content=MartinHeinz/blog-frontend&utm_campaign=Badge_Grade_Dashboard) 11 | [![Test Coverage](https://api.codeclimate.com/v1/badges/75a6e6323821fcece3ec/test_coverage)](https://codeclimate.com/github/MartinHeinz/blog-frontend/test_coverage) 12 | 13 | 14 | _TODO: About this repo_ 15 | 16 | ### Project setup 17 | ``` 18 | npm install 19 | ``` 20 | 21 | #### Compiles and hot-reloads for development 22 | ``` 23 | npm run serve 24 | ``` 25 | 26 | #### Compiles and minifies for production 27 | ``` 28 | npm run build 29 | ``` 30 | 31 | #### Run your tests 32 | ``` 33 | npm run test 34 | ``` 35 | 36 | #### Lints and fixes files 37 | ``` 38 | npm run lint 39 | ``` 40 | 41 | #### Run your end-to-end tests 42 | ``` 43 | npm run test:e2e 44 | ``` 45 | 46 | #### Run your unit tests 47 | ``` 48 | npm run test:unit 49 | ``` 50 | 51 | ### Customize configuration 52 | See [Configuration Reference](https://cli.vuejs.org/config/). 53 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /brotli.conf: -------------------------------------------------------------------------------- 1 | brotli on; 2 | brotli_comp_level 6; 3 | brotli_static on; 4 | brotli_types application/atom+xml application/javascript application/json application/rss+xml 5 | application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype 6 | application/x-font-ttf application/x-javascript application/xhtml+xml application/xml 7 | font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon 8 | image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml; 9 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /docker-base/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM nginx:mainline-alpine as builder 2 | 3 | ARG ENABLED_MODULES 4 | 5 | RUN set -ex \ 6 | && if [ "$ENABLED_MODULES" = "" ]; then \ 7 | echo "No additional modules enabled, exiting"; \ 8 | exit 1; \ 9 | fi 10 | 11 | COPY ./ /modules/ 12 | 13 | RUN set -ex \ 14 | && apk update \ 15 | && apk add linux-headers openssl-dev pcre-dev zlib-dev openssl abuild \ 16 | musl-dev libxslt libxml2-utils make mercurial gcc unzip git \ 17 | xz g++ \ 18 | # allow abuild as a root user \ 19 | && printf "#!/bin/sh\\n/usr/bin/abuild -F \"\$@\"\\n" > /usr/local/bin/abuild \ 20 | && chmod +x /usr/local/bin/abuild \ 21 | && hg clone -r ${NGINX_VERSION}-${PKG_RELEASE} https://hg.nginx.org/pkg-oss/ \ 22 | && cd pkg-oss \ 23 | && mkdir /tmp/packages \ 24 | && for module in $ENABLED_MODULES; do \ 25 | echo "Building $module for nginx-$NGINX_VERSION"; \ 26 | if [ -d /modules/$module ]; then \ 27 | echo "Building $module from user-supplied sources"; \ 28 | # check if module sources file is there and not empty 29 | if [ ! -s /modules/$module/source ]; then \ 30 | echo "No source file for $module in modules/$module/source, exiting"; \ 31 | exit 1; \ 32 | fi; \ 33 | # some modules require build dependencies 34 | if [ -f /modules/$module/build-deps ]; then \ 35 | echo "Installing $module build dependencies"; \ 36 | apk update && apk add $(cat /modules/$module/build-deps | xargs); \ 37 | fi; \ 38 | # if a module has a build dependency that is not in a distro, provide a 39 | # shell script to fetch/build/install those 40 | # note that shared libraries produced as a result of this script will 41 | # not be copied from the builder image to the main one so build static 42 | if [ -x /modules/$module/prebuild ]; then \ 43 | echo "Running prebuild script for $module"; \ 44 | /modules/$module/prebuild; \ 45 | fi; \ 46 | /pkg-oss/build_module.sh -v $NGINX_VERSION -f -y -o /tmp/packages -n $module $(cat /modules/$module/source); \ 47 | elif make -C /pkg-oss/alpine list | grep -E "^$module\s+\d+" > /dev/null; then \ 48 | echo "Building $module from pkg-oss sources"; \ 49 | cd /pkg-oss/alpine; \ 50 | make abuild-module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \ 51 | apk add $(. ./abuild-module-$module/APKBUILD; echo $makedepends;); \ 52 | make module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \ 53 | find ~/packages -type f -name "*.apk" -exec mv -v {} /tmp/packages/ \;; \ 54 | else \ 55 | echo "Don't know how to build $module module, exiting"; \ 56 | exit 1; \ 57 | fi; \ 58 | done 59 | 60 | FROM nginx:mainline-alpine 61 | ARG ENABLED_MODULES 62 | COPY --from=builder /tmp/packages /tmp/packages 63 | RUN set -ex \ 64 | && for module in $ENABLED_MODULES; do \ 65 | apk add --no-cache --allow-untrusted /tmp/packages/nginx-module-${module}-${NGINX_VERSION}*.apk; \ 66 | done \ 67 | && rm -rf /tmp/packages 68 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | cache: 4 | image: martinheinz/blog_cache:latest 5 | build: 6 | context: nginx-cache 7 | dockerfile: Dockerfile 8 | container_name: cache 9 | depends_on: 10 | - origin 11 | ports: 12 | - 80:80 13 | - 443:443 14 | environment: 15 | PROD: ${PROD} 16 | NGINX_HOST: ${NGINX_HOST} 17 | API_URL: localhost 18 | volumes: 19 | - ./data/certbot/conf:/etc/letsencrypt 20 | - ./data/certbot/www:/var/www/certbot 21 | - ${PWD}/rss.xml:/home/rss.xml 22 | - ${PWD}/sitemap.xml:/home/sitemap.xml 23 | - ${PWD}/sitemap.xml:/home/newsletter.xml 24 | origin: 25 | image: martinheinz/blog_frontend:latest 26 | build: 27 | context: . 28 | dockerfile: Dockerfile 29 | container_name: frontend 30 | depends_on: 31 | - backend 32 | expose: 33 | - 8008 34 | environment: 35 | PROD: 1 36 | API_URL: localhost 37 | volumes: 38 | - ${PWD}/rss.xml:/home/rss.xml 39 | - ${PWD}/sitemap.xml:/home/sitemap.xml 40 | - ${PWD}/sitemap.xml:/home/newsletter.xml 41 | backend: 42 | image: martinheinz/blog_backend:latest 43 | container_name: backend 44 | healthcheck: 45 | test: ["CMD", "curl", "-f", "backend"] 46 | interval: 30s 47 | timeout: 10s 48 | retries: 5 49 | depends_on: 50 | - blog_db 51 | ports: 52 | - 8080:8080 53 | volumes: 54 | - ./data/certbot/conf/live/${API_URL}:/etc/certs 55 | 56 | blog_db: 57 | image: martinheinz/blog_db:latest 58 | container_name: db 59 | healthcheck: 60 | test: ["CMD-SHELL", "pg_isready -U postgres"] 61 | interval: 10s 62 | timeout: 5s 63 | retries: 5 64 | volumes: 65 | - data:/var/lib/postgresql/data 66 | expose: 67 | - 5432 68 | environment: 69 | POSTGRES_USER: "postgres" 70 | POSTGRES_PASSWORD: "postgres" 71 | DB_NAME: "blog" 72 | POSTGRES_DB: blog 73 | PGPORT: 5432 74 | POPULATE_DB: ${POPULATE_DB} 75 | 76 | volumes: 77 | data: {} 78 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue', 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest', 12 | '^.+\\.js$': '/node_modules/babel-jest', 13 | }, 14 | transformIgnorePatterns: [ 15 | '/node_modules/', 16 | ], 17 | moduleNameMapper: { 18 | '^@/(.*)$': '/src/$1', 19 | }, 20 | snapshotSerializers: [ 21 | 'jest-serializer-vue', 22 | ], 23 | testMatch: [ 24 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', 25 | ], 26 | testURL: 'http://localhost/', 27 | watchPlugins: [ 28 | 'jest-watch-typeahead/filename', 29 | 'jest-watch-typeahead/testname', 30 | ], 31 | testResultsProcessor: 'jest-sonar-reporter', 32 | collectCoverage: true, 33 | collectCoverageFrom: [ 34 | '**/src/**/*.{js,vue}', 35 | '!**/node_modules/**', 36 | '!**/vendor/**', 37 | '!**/dist/**', 38 | '!**/tests/**', 39 | ], 40 | coverageReporters: [ 41 | 'lcov', 42 | 'text', 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /nginx-cache/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM martinheinz/nginx-brotli as runner 2 | COPY brotli.conf cache.template /etc/nginx/conf.d/ 3 | COPY nginx.conf /etc/nginx/ 4 | EXPOSE 80 5 | COPY startup.sh /home/ 6 | RUN chmod 777 /home/startup.sh 7 | CMD ["sh", "/home/startup.sh"] 8 | 9 | # docker build -t martinheinz/blog_cache -f nginx-cache/Dockerfile ./nginx-cache -------------------------------------------------------------------------------- /nginx-cache/brotli.conf: -------------------------------------------------------------------------------- 1 | brotli on; 2 | brotli_comp_level 6; 3 | brotli_static on; 4 | brotli_types application/atom+xml application/javascript application/json application/rss+xml 5 | application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype 6 | application/x-font-ttf application/x-javascript application/xhtml+xml application/xml 7 | font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon 8 | image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml; 9 | -------------------------------------------------------------------------------- /nginx-cache/cache.template: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | server_name ${NGINX_HOST}; 5 | 6 | location /.well-known/acme-challenge/ { 7 | root /var/www/certbot; 8 | } 9 | 10 | location / { 11 | return 301 https://$host$request_uri; 12 | } 13 | } 14 | 15 | upstream origin { 16 | server frontend:8008; 17 | } 18 | 19 | server { 20 | listen 443 ssl; 21 | server_name ${NGINX_HOST}; 22 | server_tokens off; 23 | 24 | ssl_certificate /etc/letsencrypt/live/${NGINX_HOST}/fullchain.pem; 25 | ssl_certificate_key /etc/letsencrypt/live/${NGINX_HOST}/privkey.pem; 26 | include /etc/letsencrypt/options-ssl-nginx.conf; 27 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 28 | 29 | # Static folder that Nginx must serve 30 | location / { 31 | add_header X-Cache-Status $upstream_cache_status; 32 | 33 | proxy_pass http://origin/; 34 | proxy_set_header Host $host; 35 | proxy_set_header X-Real-IP $remote_addr; 36 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 37 | proxy_set_header X-Forwarded-Proto https; 38 | 39 | proxy_buffering on; 40 | proxy_cache STATIC; 41 | proxy_cache_valid any 72h; 42 | proxy_cache_use_stale error timeout invalid_header updating 43 | http_500 http_502 http_503 http_504; 44 | 45 | proxy_ignore_headers X-Accel-Expires Expires Cache-Control; 46 | proxy_ignore_headers "Set-Cookie"; 47 | proxy_hide_header "Set-Cookie"; 48 | } 49 | 50 | location /rss { 51 | expires -1; 52 | index rss.xml; 53 | alias /home; 54 | } 55 | 56 | location /newsletter { 57 | expires -1; 58 | index newsletter.xml; 59 | alias /home; 60 | } 61 | 62 | location /sitemap { 63 | expires -1; 64 | index sitemap.xml; 65 | alias /home; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /nginx-cache/nginx.conf: -------------------------------------------------------------------------------- 1 | load_module "modules/ngx_http_brotli_filter_module.so"; 2 | load_module "modules/ngx_http_brotli_static_module.so"; 3 | 4 | user nginx; 5 | worker_processes auto; 6 | 7 | error_log /var/log/nginx/error.log warn; 8 | pid /var/run/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include /etc/nginx/mime.types; 18 | default_type application/octet-stream; 19 | 20 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=1y max_size=1g; 21 | 22 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 23 | '$status $body_bytes_sent "$http_referer" ' 24 | '"$http_user_agent" "$http_x_forwarded_for"'; 25 | 26 | access_log /var/log/nginx/access.log main; 27 | 28 | sendfile on; 29 | 30 | keepalive_timeout 65; 31 | 32 | include /etc/nginx/conf.d/*.conf; 33 | } 34 | -------------------------------------------------------------------------------- /nginx-cache/startup.sh: -------------------------------------------------------------------------------- 1 | rm /etc/nginx/conf.d/default.conf 2 | # Start Nginx with special option in order to run in foreground 3 | if [ "$PROD" -eq 1 ]; then 4 | echo "Setting Nginx configuration for Production Environment..." 5 | sh -c "envsubst '\$NGINX_HOST' < /etc/nginx/conf.d/cache.template > /etc/nginx/conf.d/site.conf" 6 | fi 7 | 8 | nginx -g "daemon off;" 9 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | load_module "modules/ngx_http_brotli_filter_module.so"; 2 | load_module "modules/ngx_http_brotli_static_module.so"; 3 | 4 | user nginx; 5 | worker_processes auto; 6 | 7 | error_log /var/log/nginx/error.log warn; 8 | pid /var/run/nginx.pid; 9 | 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | 16 | http { 17 | include /etc/nginx/mime.types; 18 | default_type application/octet-stream; 19 | 20 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 21 | '$status $body_bytes_sent "$http_referer" ' 22 | '"$http_user_agent" "$http_x_forwarded_for"'; 23 | 24 | access_log /var/log/nginx/access.log main; 25 | 26 | sendfile on; 27 | 28 | keepalive_timeout 65; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "test:e2e": "vue-cli-service test:e2e", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 14 | "@fortawesome/free-brands-svg-icons": "^5.15.3", 15 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 16 | "@fortawesome/vue-fontawesome": "^2.0.2", 17 | "axios": "^0.18.0", 18 | "core-js": "^3.6.5", 19 | "dayjs": "^1.10.4", 20 | "highlight.js": "^9.15.9", 21 | "lodash": "^4.17.11", 22 | "vue": "^2.6.10", 23 | "vue-headful": "^2.0.1", 24 | "vue-highlight.js": "^3.1.0", 25 | "vue-meta": "^2.2.1", 26 | "vue-router": "latest", 27 | "vue-social-sharing": "^2.4.3", 28 | "vue-spinner": "^1.0.3", 29 | "vuetify": "^1.5.14", 30 | "vuex": "^3.1.1" 31 | }, 32 | "devDependencies": { 33 | "@vue/cli-plugin-babel": "^4.5.12", 34 | "@vue/cli-plugin-eslint": "^4.5.12", 35 | "@vue/cli-plugin-unit-jest": "^4.5.12", 36 | "@vue/cli-service": "^4.5.12", 37 | "@vue/eslint-config-airbnb": "^5.0.2", 38 | "@vue/test-utils": "1.0.0-beta.29", 39 | "axios-mock-adapter": "^1.17.0", 40 | "babel-core": "7.0.0-bridge.0", 41 | "babel-eslint": "^10.1.0", 42 | "babel-jest": "^23.6.0", 43 | "closure-webpack-plugin": "^2.5.0", 44 | "css-minimizer-webpack-plugin": "^1.3.0", 45 | "eslint": "^6.7.2", 46 | "eslint-plugin-import": "^2.20.2", 47 | "eslint-plugin-vue": "^6.2.2", 48 | "google-closure-compiler": "^20210302.0.0", 49 | "jest-sonar-reporter": "^2.0.0", 50 | "stylus": "^0.54.5", 51 | "stylus-loader": "^3.0.2", 52 | "terser-webpack-plugin": "^4.2.3", 53 | "vue-jest": "^3.0.4", 54 | "vue-template-compiler": "^2.5.21", 55 | "vuetify-loader": "^1.7.2", 56 | "webpack": "^4.46.0", 57 | "webpack-cli": "^4.6.0", 58 | "webpack-font-preload-plugin": "^1.2.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prod.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8008; 3 | server_name localhost; 4 | server_tokens off; 5 | 6 | 7 | # Static folder that Nginx must serve 8 | location / { 9 | try_files $uri $uri/ /index.html; 10 | root /home/html; 11 | auth_basic off; 12 | 13 | expires 72h; 14 | proxy_hide_header 'Cache-Control'; 15 | add_header 'Cache-Control' "public, max-age=259200"; 16 | } 17 | 18 | location ~* \.(?:cur|jpe?g|gif|htc|ico|png|otf|ttf|eot|woff|woff2|svg)$ { 19 | root /home/html; 20 | access_log off; 21 | add_header Cache-Control public; 22 | expires max; 23 | 24 | tcp_nodelay off; 25 | } 26 | 27 | location ~* \.(?:html)$ { 28 | root /home/html; 29 | access_log off; 30 | add_header Cache-Control public; 31 | expires 2h; 32 | 33 | tcp_nodelay off; 34 | } 35 | 36 | 37 | location ~* \.(?:css|js)$ { 38 | root /home/html; 39 | access_log off; 40 | add_header Cache-Control public; 41 | expires 30d; 42 | 43 | tcp_nodelay off; 44 | } 45 | 46 | 47 | # In order to avoid favicon errors on some navigators like IE 48 | # which would pollute Nginx logs (do use the "=") 49 | location = /favicon.ico { 50 | access_log off; 51 | log_not_found off; 52 | } 53 | 54 | location ~ ^/(api|editor) { 55 | auth_basic "Administrator’s Area"; 56 | auth_basic_user_file /etc/nginx/.htpasswd; 57 | } 58 | 59 | # robots.txt file generated on the fly 60 | location /robots.txt { 61 | return 200 "User-agent: *\nDisallow: \n\nUser-agent: *\nDisallow: /api/v1/"; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/blog-frontend/e584d2e9704c63f56f78b57232269a8271c808f9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 43 | 44 | 63 | 64 | 65 | 72 | 73 | 74 | 77 |
78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 8080; 4 | server_name localhost; 5 | 6 | # In order to avoid favicon errors on some navigators like IE 7 | # which would pollute Nginx logs (do use the "=") 8 | location = /favicon.ico { 9 | access_log off; 10 | log_not_found off; 11 | } 12 | 13 | # Static folder that Nginx must serve 14 | location / { 15 | try_files $uri $uri/ /index.html; 16 | root /home/html; 17 | auth_basic off; 18 | 19 | add_header X-Cache-Status $upstream_cache_status; 20 | 21 | proxy_pass https://martinheinz.dev; 22 | proxy_set_header Host $host; 23 | proxy_buffering on; 24 | proxy_cache STATIC; 25 | proxy_cache_valid any 72h; 26 | proxy_cache_use_stale error timeout invalid_header updating 27 | http_500 http_502 http_503 http_504; 28 | 29 | proxy_ignore_headers X-Accel-Expires Expires Cache-Control; 30 | proxy_ignore_headers "Set-Cookie"; 31 | proxy_hide_header "Set-Cookie"; 32 | } 33 | 34 | location /api { 35 | auth_basic "Administrator’s Area"; 36 | auth_basic_user_file /etc/nginx/.htpasswd; 37 | } 38 | 39 | # robots.txt file generated on the fly 40 | location /robots.txt { 41 | return 200 "User-agent: *\nDisallow: \n\nUser-agent: *\nDisallow: /api/v1/"; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=MartinHeinz_blog-frontend 2 | sonar.organization=martinheinz-github 3 | sonar.projectName=blog-frontend 4 | sonar.projectVersion=0.0.1 5 | 6 | sonar.login=095619414807ebe647d1d413b3a9df3b081642dc 7 | sonar.password= 8 | 9 | sonar.links.homepage=https://github.com/MartinHeinz/blog-frontend 10 | sonar.links.ci=https://travis-ci.com/MartinHeinz/blog-frontend 11 | sonar.links.scm=https://github.com/MartinHeinz/blog-frontend 12 | 13 | sonar.sources=src 14 | sonar.tests=tests 15 | sonar.binaries=dist 16 | 17 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info 18 | sonar.testExecutionReportPaths=test-report.xml 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 55 | 56 | 94 | -------------------------------------------------------------------------------- /src/assets/MesloLGS-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/blog-frontend/e584d2e9704c63f56f78b57232269a8271c808f9/src/assets/MesloLGS-Regular.woff -------------------------------------------------------------------------------- /src/assets/MesloLGS-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/blog-frontend/e584d2e9704c63f56f78b57232269a8271c808f9/src/assets/MesloLGS-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/OxygenMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MartinHeinz/blog-frontend/e584d2e9704c63f56f78b57232269a8271c808f9/src/assets/OxygenMono-Regular.woff2 -------------------------------------------------------------------------------- /src/assets/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | .inline { 5 | display: inline; 6 | } 7 | .block { 8 | display: block; 9 | } 10 | .inline-block { 11 | display: inline-block; 12 | } 13 | .table { 14 | display: table; 15 | } 16 | .table-cell { 17 | display: table-cell; 18 | } 19 | .overflow-hidden { 20 | overflow: hidden; 21 | } 22 | .overflow-scroll { 23 | overflow: scroll; 24 | } 25 | .overflow-auto { 26 | overflow: auto; 27 | } 28 | .clearfix:before, 29 | .clearfix:after { 30 | display: table; 31 | content: " "; 32 | } 33 | .clearfix:after { 34 | clear: both; 35 | } 36 | .left { 37 | float: left; 38 | } 39 | .right { 40 | float: right; 41 | } 42 | .fit { 43 | max-width: 100%; 44 | } 45 | .truncate { 46 | display: inline-block; 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | white-space: nowrap; 50 | } 51 | .max-width-1 { 52 | max-width: 24rem; 53 | } 54 | .max-width-2 { 55 | max-width: 32rem; 56 | } 57 | .max-width-3 { 58 | max-width: 48rem; 59 | } 60 | .max-width-4 { 61 | max-width: 64rem; 62 | } 63 | .border-box { 64 | box-sizing: border-box; 65 | } 66 | .m0 { 67 | margin: 0; 68 | } 69 | .mt0 { 70 | margin-top: 0; 71 | } 72 | .mr0 { 73 | margin-right: 0; 74 | } 75 | .mb0 { 76 | margin-bottom: 0; 77 | } 78 | .ml0 { 79 | margin-left: 0; 80 | } 81 | .mx0 { 82 | margin-right: 0; 83 | margin-left: 0; 84 | } 85 | .my0 { 86 | margin-top: 0; 87 | margin-bottom: 0; 88 | } 89 | .m1 { 90 | margin: 0.5rem; 91 | } 92 | .mt1 { 93 | margin-top: 0.5rem; 94 | } 95 | .mr1 { 96 | margin-right: 0.5rem; 97 | } 98 | .mb1 { 99 | margin-bottom: 0.5rem; 100 | } 101 | .ml1 { 102 | margin-left: 0.5rem; 103 | } 104 | .mx1 { 105 | margin-right: 0.5rem; 106 | margin-left: 0.5rem; 107 | } 108 | .my1 { 109 | margin-top: 0.5rem; 110 | margin-bottom: 0.5rem; 111 | } 112 | .m2 { 113 | margin: 1rem; 114 | } 115 | .mt2 { 116 | margin-top: 1rem; 117 | } 118 | .mr2 { 119 | margin-right: 1rem; 120 | } 121 | .mb2 { 122 | margin-bottom: 1rem; 123 | } 124 | .ml2 { 125 | margin-left: 1rem; 126 | } 127 | .mx2 { 128 | margin-right: 1rem; 129 | margin-left: 1rem; 130 | } 131 | .my2 { 132 | margin-top: 1rem; 133 | margin-bottom: 1rem; 134 | } 135 | .m3 { 136 | margin: 2rem; 137 | } 138 | .mt3 { 139 | margin-top: 2rem; 140 | } 141 | .mr3 { 142 | margin-right: 2rem; 143 | } 144 | .mb3 { 145 | margin-bottom: 2rem; 146 | } 147 | .ml3 { 148 | margin-left: 2rem; 149 | } 150 | .mx3 { 151 | margin-right: 2rem; 152 | margin-left: 2rem; 153 | } 154 | .my3 { 155 | margin-top: 2rem; 156 | margin-bottom: 2rem; 157 | } 158 | .m4 { 159 | margin: 4rem; 160 | } 161 | .mt4 { 162 | margin-top: 4rem; 163 | } 164 | .mr4 { 165 | margin-right: 4rem; 166 | } 167 | .mb4 { 168 | margin-bottom: 4rem; 169 | } 170 | .ml4 { 171 | margin-left: 4rem; 172 | } 173 | .mx4 { 174 | margin-right: 4rem; 175 | margin-left: 4rem; 176 | } 177 | .my4 { 178 | margin-top: 4rem; 179 | margin-bottom: 4rem; 180 | } 181 | .mxn1 { 182 | margin-right: -0.5rem; 183 | margin-left: -0.5rem; 184 | } 185 | .mxn2 { 186 | margin-right: -1rem; 187 | margin-left: -1rem; 188 | } 189 | .mxn3 { 190 | margin-right: -2rem; 191 | margin-left: -2rem; 192 | } 193 | .mxn4 { 194 | margin-right: -4rem; 195 | margin-left: -4rem; 196 | } 197 | .ml-auto { 198 | margin-left: auto; 199 | } 200 | .mr-auto { 201 | margin-right: auto; 202 | } 203 | .mx-auto { 204 | margin-right: auto; 205 | margin-left: auto; 206 | } 207 | .p0 { 208 | padding: 0; 209 | } 210 | .pt0 { 211 | padding-top: 0; 212 | } 213 | .pr0 { 214 | padding-right: 0; 215 | } 216 | .pb0 { 217 | padding-bottom: 0; 218 | } 219 | .pl0 { 220 | padding-left: 0; 221 | } 222 | .px0 { 223 | padding-right: 0; 224 | padding-left: 0; 225 | } 226 | .py0 { 227 | padding-top: 0; 228 | padding-bottom: 0; 229 | } 230 | .p1 { 231 | padding: 0.5rem; 232 | } 233 | .pt1 { 234 | padding-top: 0.5rem; 235 | } 236 | .pr1 { 237 | padding-right: 0.5rem; 238 | } 239 | .pb1 { 240 | padding-bottom: 0.5rem; 241 | } 242 | .pl1 { 243 | padding-left: 0.5rem; 244 | } 245 | .py1 { 246 | padding-top: 0.5rem; 247 | padding-bottom: 0.5rem; 248 | } 249 | .px1 { 250 | padding-right: 0.5rem; 251 | padding-left: 0.5rem; 252 | } 253 | .p2 { 254 | padding: 1rem; 255 | } 256 | .pt2 { 257 | padding-top: 1rem; 258 | } 259 | .pr2 { 260 | padding-right: 1rem; 261 | } 262 | .pb2 { 263 | padding-bottom: 1rem; 264 | } 265 | .pl2 { 266 | padding-left: 1rem; 267 | } 268 | .py2 { 269 | padding-top: 1rem; 270 | padding-bottom: 1rem; 271 | } 272 | .px2 { 273 | padding-right: 1rem; 274 | padding-left: 1rem; 275 | } 276 | .p3 { 277 | padding: 2rem; 278 | } 279 | .pt3 { 280 | padding-top: 2rem; 281 | } 282 | .pr3 { 283 | padding-right: 2rem; 284 | } 285 | .pb3 { 286 | padding-bottom: 2rem; 287 | } 288 | .pl3 { 289 | padding-left: 2rem; 290 | } 291 | .py3 { 292 | padding-top: 2rem; 293 | padding-bottom: 2rem; 294 | } 295 | .px3 { 296 | padding-right: 2rem; 297 | padding-left: 2rem; 298 | } 299 | .p4 { 300 | padding: 4rem; 301 | } 302 | .pt4 { 303 | padding-top: 4rem; 304 | } 305 | .pr4 { 306 | padding-right: 4rem; 307 | } 308 | .pb4 { 309 | padding-bottom: 4rem; 310 | } 311 | .pl4 { 312 | padding-left: 4rem; 313 | } 314 | .py4 { 315 | padding-top: 4rem; 316 | padding-bottom: 4rem; 317 | } 318 | .px4 { 319 | padding-right: 4rem; 320 | padding-left: 4rem; 321 | } 322 | body h1, 323 | body .h1 { 324 | display: block; 325 | margin-top: 3rem; 326 | margin-bottom: 1rem; 327 | color: #2bbc8a; 328 | letter-spacing: 0.01em; 329 | font-weight: 700; 330 | font-style: normal; 331 | font-size: 1.5em; 332 | -moz-osx-font-smoothing: grayscale; 333 | -webkit-font-smoothing: antialiased; 334 | } 335 | body h2, 336 | body .h2 { 337 | position: relative; 338 | display: block; 339 | margin-top: 2rem; 340 | margin-bottom: 0.5rem; 341 | color: #eee; 342 | text-transform: none; 343 | letter-spacing: normal; 344 | font-weight: bold; 345 | font-size: 1rem; 346 | } 347 | body h3 { 348 | color: #eee; 349 | text-decoration: underline; 350 | font-weight: bold; 351 | font-size: 0.9rem; 352 | } 353 | body h4, 354 | body h5, 355 | body h6 { 356 | display: inline; 357 | color: #ccc; 358 | text-decoration: underline; 359 | font-weight: normal; 360 | font-size: 0.8rem; 361 | } 362 | body h3, 363 | body h4, 364 | body h5, 365 | body h6 { 366 | margin-top: 0.9rem; 367 | margin-bottom: 0.5rem; 368 | } 369 | body hr { 370 | border: 1px dashed #ccc; 371 | margin: 0; 372 | } 373 | body strong { 374 | font-weight: bold; 375 | } 376 | body em, 377 | body cite { 378 | font-style: italic; 379 | } 380 | body sup, 381 | body sub { 382 | position: relative; 383 | vertical-align: baseline; 384 | font-size: 0.75em; 385 | line-height: 0; 386 | } 387 | body sup { 388 | top: -0.5em; 389 | } 390 | body sub { 391 | bottom: -0.2em; 392 | } 393 | body small { 394 | font-size: 0.85em; 395 | } 396 | body acronym, 397 | body abbr { 398 | border-bottom: 1px dotted; 399 | } 400 | body ul, 401 | body ol, 402 | body dl { 403 | line-height: 1.725; 404 | } 405 | body ul ul, 406 | body ol ul, 407 | body ul ol, 408 | body ol ol { 409 | margin-top: 0; 410 | margin-bottom: 0; 411 | } 412 | body ol { 413 | list-style: decimal; 414 | } 415 | body dt { 416 | font-weight: bold; 417 | } 418 | body table { 419 | width: 100%; 420 | border-collapse: collapse; 421 | text-align: left; 422 | font-size: 12px; 423 | overflow: auto; 424 | display: block; 425 | } 426 | body th { 427 | padding: 8px; 428 | border-bottom: 1px dashed #666; 429 | color: #eee; 430 | font-weight: bold; 431 | font-size: 13px; 432 | } 433 | body td { 434 | padding: 9px 8px 0; 435 | border-bottom: none; 436 | } 437 | @font-face { 438 | font-style: normal; 439 | font-family: "Oxygen"; 440 | src: local("Oxygen"), url("./OxygenMono-Regular.woff2") format("woff2"); 441 | font-display: swap; 442 | unicode-range: U+000-5FF; 443 | } 444 | *, 445 | *:before, 446 | *:after { 447 | box-sizing: border-box; 448 | } 449 | body { 450 | height: 100%; 451 | background-color: #1d1f21; 452 | color: #c9cacc; 453 | font-display: swap; 454 | font-weight: 400; 455 | font-size: 14px; 456 | font-family: 'Oxygen', monospace; 457 | line-height: 1.725; 458 | text-rendering: geometricPrecision; 459 | flex: 1; 460 | -moz-osx-font-smoothing: grayscale; 461 | -webkit-font-smoothing: antialiased; 462 | 463 | padding-right: 2rem; 464 | padding-left: 2rem; 465 | margin: 0 auto; 466 | } 467 | .content p { 468 | hyphens: auto; 469 | -moz-hyphens: auto; 470 | -ms-hyphens: auto; 471 | -webkit-hyphens: auto; 472 | } 473 | .content code { 474 | hyphens: manual; 475 | -moz-hyphens: manual; 476 | -ms-hyphens: manual; 477 | -webkit-hyphens: manual; 478 | } 479 | .content a { 480 | color: #c9cacc; 481 | text-decoration: none; 482 | background-image: linear-gradient(transparent, transparent 5px, #c9cacc 5px, #c9cacc); 483 | background-position: bottom; 484 | background-size: 100% 6px; 485 | background-repeat: repeat-x; 486 | } 487 | .content a:hover { 488 | background-image: linear-gradient(transparent, transparent 4px, #d480aa 4px, #d480aa); 489 | } 490 | .content a.icon { 491 | background: none; 492 | } 493 | .content a.icon:hover { 494 | color: #d480aa; 495 | } 496 | .content h1 a, 497 | .content .h1 a, 498 | .content h2 a, 499 | .content h3 a, 500 | .content h4 a, 501 | .content h5 a, 502 | .content h6 a { 503 | background: none; 504 | color: inherit; 505 | text-decoration: none; 506 | } 507 | 508 | .content h6 a { 509 | background: none; 510 | color: inherit; 511 | text-decoration: none; 512 | } 513 | 514 | @media (min-width: 540px) { 515 | .image-wrap { 516 | flex-direction: row; 517 | margin-bottom: 2rem; 518 | } 519 | .image-wrap .image-block { 520 | flex: 1 0 35%; 521 | margin-right: 2rem; 522 | } 523 | .image-wrap p { 524 | flex: 1 0 65%; 525 | } 526 | } 527 | .max-width { 528 | max-width: 64rem; 529 | } 530 | @media (max-width: 480px) { 531 | .px3 { 532 | padding-right: 1rem; 533 | padding-left: 1rem; 534 | } 535 | .my4 { 536 | margin-top: 2rem; 537 | margin-bottom: 2rem; 538 | } 539 | } 540 | @media (min-width: 480px) { 541 | p { 542 | text-align: justify; 543 | } 544 | } 545 | #header { 546 | margin: 0 auto 2rem; 547 | width: 100%; 548 | } 549 | #header h1, 550 | #header .h1 { 551 | margin-top: 0; 552 | margin-bottom: 0; 553 | color: #c9cacc; 554 | letter-spacing: 0.01em; 555 | font-weight: 700; 556 | font-style: normal; 557 | font-size: 1.5rem; 558 | line-height: 2rem; 559 | -moz-osx-font-smoothing: grayscale; 560 | -webkit-font-smoothing: antialiased; 561 | } 562 | #header a { 563 | background: none; 564 | color: inherit; 565 | text-decoration: none; 566 | } 567 | #header #logo { 568 | display: inline-block; 569 | float: left; 570 | margin-right: 20px; 571 | width: 50px; 572 | height: 50px; 573 | border-radius: 5px; 574 | filter: grayscale(100%); 575 | background-size: 50px 50px; 576 | background-repeat: no-repeat; 577 | -webkit-filter: grayscale(100%); 578 | } 579 | #header #nav { 580 | color: #2bbc8a; 581 | letter-spacing: 0.01em; 582 | font-weight: 200; 583 | font-style: normal; 584 | font-size: 0.8rem; 585 | } 586 | #header #nav ul { 587 | margin: 0; 588 | padding: 0; 589 | list-style-type: none; 590 | line-height: 15px; 591 | } 592 | #header #nav ul a { 593 | margin-right: 15px; 594 | color: #2bbc8a; 595 | } 596 | #header #nav ul a:hover { 597 | background-image: linear-gradient(transparent, transparent 5px, #2bbc8a 5px, #2bbc8a); 598 | background-position: bottom; 599 | background-size: 100% 6px; 600 | background-repeat: repeat-x; 601 | } 602 | #header #nav ul li { 603 | display: inline-block; 604 | margin-right: 15px; 605 | border-right: 1px dotted #2bbc8a; 606 | vertical-align: middle; 607 | } 608 | #header #nav ul .icon { 609 | display: none; 610 | } 611 | #header #nav ul li:last-child { 612 | margin-right: 0; 613 | border-right: 0; 614 | } 615 | #header #nav ul li:last-child a { 616 | margin-right: 0; 617 | } 618 | #header:hover #logo { 619 | filter: none; 620 | -webkit-filter: none; 621 | } 622 | @media screen and (max-width: 480px) { 623 | #header #title { 624 | display: table; 625 | margin-right: 5rem; 626 | min-height: 50px; 627 | } 628 | #header #title h1 { 629 | display: table-cell; 630 | vertical-align: middle; 631 | } 632 | #header #nav ul a:hover { 633 | background: none; 634 | } 635 | #header #nav ul li { 636 | display: none; 637 | border-right: 0; 638 | } 639 | #header #nav ul li.icon { 640 | position: absolute; 641 | top: 77px; 642 | right: 1rem; 643 | display: inline-block; 644 | } 645 | #header #nav ul.responsive li { 646 | display: block; 647 | } 648 | #header #nav li:not(:first-child) { 649 | padding-top: 1rem; 650 | padding-left: 70px; 651 | font-size: 1rem; 652 | } 653 | } 654 | #header-post { 655 | position: fixed; 656 | top: 2rem; 657 | right: 0; 658 | display: inline-block; 659 | float: right; 660 | z-index: 100; 661 | } 662 | #header-post a { 663 | background: none; 664 | color: inherit; 665 | text-decoration: none; 666 | } 667 | #header-post a.icon { 668 | background: none; 669 | } 670 | #header-post a.icon:hover { 671 | color: #d480aa; 672 | } 673 | #header-post ol { 674 | list-style-type: none; 675 | } 676 | #header-post ul { 677 | display: inline-block; 678 | margin: 0; 679 | padding: 0; 680 | list-style-type: none; 681 | } 682 | #header-post ul li { 683 | display: inline-block; 684 | margin-right: 15px; 685 | vertical-align: middle; 686 | } 687 | #header-post ul li:last-child { 688 | margin-right: 0; 689 | } 690 | #header-post #menu-icon { 691 | float: right; 692 | margin-right: 2rem; 693 | margin-left: 25px; 694 | } 695 | 696 | #header-post #menu-icon-tablet { 697 | float: right; 698 | margin-right: 2rem; 699 | margin-left: 25px; 700 | } 701 | 702 | #header-post #menu { 703 | visibility: hidden; 704 | } 705 | 706 | #header-post #toc { 707 | float: right; 708 | clear: both; 709 | overflow: auto; 710 | margin-top: 1rem; 711 | padding-right: 2rem; 712 | max-width: 20em; 713 | max-height: calc(95vh - 7rem); 714 | text-align: right; 715 | } 716 | #header-post #toc div:hover { 717 | color: #d480aa; 718 | } 719 | #header-post #toc .toc-level-1 > .toc-link { 720 | display: none; 721 | } 722 | #header-post #toc .toc-level-2 { 723 | color: #c9cacc; 724 | font-size: 0.8rem; 725 | } 726 | #header-post #toc .toc-level-2:before { 727 | color: #2bbc8a; 728 | content: "#"; 729 | } 730 | #header-post #toc .toc-level-3 { 731 | color: #666; 732 | font-size: 0.7rem; 733 | } 734 | #header-post #toc .toc-level-4 { 735 | color: #525252; 736 | font-size: 0.4rem; 737 | } 738 | #header-post #toc .toc-level-5 { 739 | display: none; 740 | } 741 | #header-post #toc .toc-level-6 { 742 | display: none; 743 | } 744 | #header-post #toc .toc-number { 745 | display: none; 746 | } 747 | @media screen and (max-width: 600px) { 748 | #header-post { 749 | display: none; 750 | } 751 | } 752 | @media screen and (max-width: 899px) { 753 | #header-post #menu-icon { 754 | display: none; 755 | } 756 | .icon-active { 757 | color: #c9cacc; 758 | } 759 | #actions .info { 760 | display: none; 761 | } 762 | } 763 | @media screen and (max-width: 1199px) { 764 | #header-post #toc { 765 | display: none; 766 | } 767 | } 768 | @media screen and (min-width: 900px) { 769 | #header-post #menu-icon-tablet { 770 | display: none !important; 771 | } 772 | #header-post #top-icon-tablet { 773 | display: none !important; 774 | } 775 | } 776 | @media screen and (min-width: 1199px) { 777 | #header-post #actions { 778 | width: auto; 779 | } 780 | #header-post #actions ul { 781 | display: inline-block; 782 | float: right; 783 | } 784 | #header-post #actions .info { 785 | display: inline; 786 | float: left; 787 | margin-right: 2rem; 788 | font-style: italic; 789 | } 790 | } 791 | .post-list { 792 | padding: 0; 793 | } 794 | .post-list .post-item { 795 | margin-bottom: 1rem; 796 | margin-left: 0; 797 | list-style-type: none; 798 | } 799 | .project-list { 800 | padding: 0; 801 | list-style: none; 802 | } 803 | .project-list .project-item { 804 | margin-bottom: 5px; 805 | } 806 | .project-list .project-item p { 807 | display: inline; 808 | } 809 | article header .posttitle { 810 | margin-top: 0; 811 | margin-bottom: 0; 812 | text-transform: none; 813 | font-size: 1.5em; 814 | line-height: 1.25; 815 | } 816 | article header .meta { 817 | margin-top: 0; 818 | margin-bottom: 1rem; 819 | } 820 | article header .meta * { 821 | color: #ccc; 822 | font-size: 0.85rem; 823 | } 824 | article header .author { 825 | text-transform: uppercase; 826 | letter-spacing: 0.01em; 827 | font-weight: 700; 828 | } 829 | article header .postdate { 830 | display: inline; 831 | } 832 | article .content h2:before { 833 | position: absolute; 834 | top: -4px; 835 | left: -1rem; 836 | color: #2bbc8a; 837 | content: "#"; 838 | font-weight: bold; 839 | font-size: 1.2rem; 840 | } 841 | article .content img, 842 | article .content video { 843 | display: block; 844 | margin: auto; 845 | max-width: 100%; 846 | height: auto; 847 | } 848 | article .content .video-container { 849 | position: relative; 850 | overflow: hidden; 851 | padding-top: 56.25%; 852 | height: 0; 853 | } 854 | article .content .video-container iframe, 855 | article .content .video-container object, 856 | article .content .video-container embed { 857 | position: absolute; 858 | top: 0; 859 | left: 0; 860 | margin-top: 0; 861 | width: 100%; 862 | height: 100%; 863 | } 864 | article .content blockquote { 865 | margin: 1rem 10px; 866 | padding: 0.5em 10px; 867 | background: inherit; 868 | color: #ccffb6; 869 | quotes: "\201C" "\201D" "\2018" "\2019"; 870 | font-weight: bold; 871 | } 872 | article .content blockquote p { 873 | margin: 0; 874 | } 875 | article .content blockquote:before { 876 | margin-right: 0.25em; 877 | color: #ccffb6; 878 | content: "\201C"; 879 | vertical-align: -0.4em; 880 | font-size: 2em; 881 | line-height: 0.1em; 882 | } 883 | article .content blockquote footer { 884 | margin: 0; 885 | color: #666; 886 | font-size: 11px; 887 | } 888 | article .content blockquote footer a { 889 | background-image: linear-gradient(transparent, transparent 5px, #666 5px, #666); 890 | color: #666; 891 | } 892 | article .content blockquote footer a:hover { 893 | background-image: linear-gradient(transparent, transparent 4px, #858585 4px, #858585); 894 | color: #858585; 895 | } 896 | article .content blockquote footer cite:before { 897 | padding: 0 0.5em; 898 | content: "—"; 899 | } 900 | article .content .pullquote { 901 | margin: 0; 902 | width: 45%; 903 | text-align: left; 904 | } 905 | article .content .pullquote.left { 906 | margin-right: 1em; 907 | margin-left: 0.5em; 908 | } 909 | article .content .pullquote.right { 910 | margin-right: 0.5em; 911 | margin-left: 1em; 912 | } 913 | article .content .caption { 914 | position: relative; 915 | display: block; 916 | margin-top: 0.5em; 917 | color: #808080; 918 | text-align: center; 919 | font-size: 0.9em; 920 | } 921 | .posttitle { 922 | text-transform: none; 923 | font-size: 1.5em; 924 | line-height: 1.25; 925 | } 926 | #archive .post-list { 927 | padding: 0; 928 | } 929 | #archive .post-list .post-item { 930 | margin-bottom: 1rem; 931 | margin-left: 0; 932 | list-style-type: none; 933 | } 934 | #archive .post-list .post-item .meta { 935 | display: block; 936 | margin-right: 16px; 937 | min-width: 100px; 938 | color: #666; 939 | font-size: 14px; 940 | } 941 | @media (min-width: 480px) { 942 | #archive .post-list .post-item { 943 | display: flex; 944 | margin-bottom: 5px; 945 | margin-left: 1rem; 946 | } 947 | #archive .post-list .post-item .meta { 948 | text-align: left; 949 | } 950 | } 951 | .blog-post-comments { 952 | margin-top: 4rem; 953 | } 954 | .pagination { 955 | display: inline-block; 956 | margin-top: 2rem; 957 | width: 100%; 958 | text-align: center; 959 | } 960 | .pagination .page-number { 961 | color: #c9cacc; 962 | font-size: 0.8rem; 963 | } 964 | .pagination a { 965 | padding: 4px 6px; 966 | border-radius: 5px; 967 | background-image: none; 968 | color: #c9cacc; 969 | text-decoration: none; 970 | } 971 | .pagination a:hover { 972 | background-image: none; 973 | } 974 | .pagination a:hover:not(.active) { 975 | color: #eee; 976 | } 977 | .search-input { 978 | padding: 4px 7px; 979 | width: 100%; 980 | outline: none; 981 | border: solid 1px #ccc; 982 | border-radius: 5px; 983 | background-color: #1d1f21; 984 | color: #c9cacc; 985 | font-size: 1.2rem; 986 | -webkit-border-radius: 5px; 987 | -moz-border-radius: 5px; 988 | } 989 | .search-input:focus { 990 | border: solid 1px #2bbc8a; 991 | } 992 | #search-result ul.search-result-list { 993 | padding: 0; 994 | list-style-type: none; 995 | } 996 | #search-result li { 997 | margin: 2em auto; 998 | } 999 | #search-result a.search-result-title { 1000 | background-image: none; 1001 | color: #c9cacc; 1002 | text-transform: capitalize; 1003 | font-weight: bold; 1004 | line-height: 1.2; 1005 | } 1006 | #search-result p.search-result { 1007 | overflow: hidden; 1008 | margin: 0.4em auto; 1009 | max-height: 13em; 1010 | text-align: justify; 1011 | font-size: 0.8em; 1012 | } 1013 | #search-result em.search-keyword { 1014 | border-bottom: 1px dashed #d480aa; 1015 | color: #d480aa; 1016 | font-weight: bold; 1017 | } 1018 | .search-no-result { 1019 | display: none; 1020 | padding-bottom: 0.5em; 1021 | color: #c9cacc; 1022 | } 1023 | #tag-cloud .tag-cloud-title { 1024 | color: #666; 1025 | } 1026 | #tag-cloud .tag-cloud-tags { 1027 | clear: both; 1028 | text-align: center; 1029 | } 1030 | #tag-cloud .tag-cloud-tags a { 1031 | display: inline-block; 1032 | margin: 10px; 1033 | } 1034 | #categories .category-list-title { 1035 | color: #666; 1036 | } 1037 | #categories .category-list .category-list-item .category-list-count { 1038 | color: #666; 1039 | } 1040 | #categories .category-list .category-list-item .category-list-count:before { 1041 | content: " ("; 1042 | } 1043 | #categories .category-list .category-list-item .category-list-count:after { 1044 | content: ")"; 1045 | } 1046 | 1047 | pre code { 1048 | border: 1px dotted #666; 1049 | } 1050 | 1051 | .inline { 1052 | overflow-x: auto; 1053 | font-size: 13px; 1054 | font-family: 'Oxygen', monospace; 1055 | line-height: 22px; 1056 | padding: 0 5px; 1057 | background: #212326; 1058 | border: 1px dotted #666; 1059 | border-radius: 2px; 1060 | -webkit-border-radius: 2px; 1061 | } 1062 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | export const API_URL = `https://${process.env.VUE_APP_API_URL}:8080/api/v1/`; 2 | export default API_URL; 3 | -------------------------------------------------------------------------------- /src/components/About.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 78 | 79 | 117 | -------------------------------------------------------------------------------- /src/components/BaseFooter.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 187 | -------------------------------------------------------------------------------- /src/components/BaseFooterItem.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/components/BaseMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /src/components/BaseMenuItem.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /src/components/BaseTag.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 34 | -------------------------------------------------------------------------------- /src/components/Blog.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /src/components/BookList.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /src/components/BookListItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /src/components/Contact.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 39 | 40 | 43 | -------------------------------------------------------------------------------- /src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | 64 | -------------------------------------------------------------------------------- /src/components/MenuActions.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 96 | 97 | 124 | -------------------------------------------------------------------------------- /src/components/NavFooter.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 100 | 101 | 155 | -------------------------------------------------------------------------------- /src/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 82 | 83 | 156 | -------------------------------------------------------------------------------- /src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/components/Post.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/components/PostEditor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/components/PostHeader.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 67 | 68 | 137 | -------------------------------------------------------------------------------- /src/components/PostList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 54 | 55 | 66 | -------------------------------------------------------------------------------- /src/components/PostListItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /src/components/PostListPage.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /src/components/Project.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | 41 | 69 | -------------------------------------------------------------------------------- /src/components/Projects.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 50 | 51 | 54 | -------------------------------------------------------------------------------- /src/components/SocialSharingList.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 69 | 70 | 88 | -------------------------------------------------------------------------------- /src/components/SubscribePage.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 118 | 119 | 173 | -------------------------------------------------------------------------------- /src/components/TableOfContents.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { dom, library } from '@fortawesome/fontawesome-svg-core'; 2 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; 3 | import { 4 | faHeartbeat, faFire, faLightbulb, faChevronLeft, faChevronRight, faChevronUp, faRss, faShareAlt, faTag, faBars, faPaperPlane, 5 | } from '@fortawesome/free-solid-svg-icons'; 6 | import { 7 | faGithub, faLinkedin, faTwitter, faReddit, faFacebook, faWhatsapp, faWeibo, faVk, 8 | } from '@fortawesome/free-brands-svg-icons'; 9 | import Vue from 'vue'; 10 | import VueMeta from 'vue-meta'; 11 | import VueHighlightJS from 'vue-highlight.js'; 12 | import upperFirst from 'lodash/upperFirst'; 13 | import camelCase from 'lodash/camelCase'; 14 | import Vuetify from 'vuetify/lib'; 15 | import router from '@/router'; 16 | import store from '@/store'; 17 | import vueHeadful from 'vue-headful'; 18 | 19 | // Highlight.js languages (Only required languages) 20 | import python from 'highlight.js/lib/languages/python'; 21 | import shell from 'highlight.js/lib/languages/shell'; 22 | import dockerfile from 'highlight.js/lib/languages/dockerfile'; 23 | import go from 'highlight.js/lib/languages/go'; 24 | import javascript from 'highlight.js/lib/languages/javascript'; 25 | import makefile from 'highlight.js/lib/languages/makefile'; 26 | import yaml from 'highlight.js/lib/languages/yaml'; 27 | import bash from 'highlight.js/lib/languages/bash'; 28 | import ini from 'highlight.js/lib/languages/ini'; 29 | import json from 'highlight.js/lib/languages/json'; 30 | 31 | // import 'highlight.js/styles/an-old-hope.css'; 32 | // import 'highlight.js/styles/darcula.css'; 33 | // import 'highlight.js/styles/nord.css'; 34 | // import 'highlight.js/styles/vs2015.css'; 35 | import 'highlight.js/styles/atom-one-dark-reasonable.css'; 36 | 37 | import App from '@/App.vue'; 38 | 39 | import '@/assets/style.css'; 40 | import localizableFormat from 'dayjs/plugin/localizedFormat'; 41 | import dayjs from 'dayjs'; 42 | 43 | dayjs.extend(localizableFormat); 44 | 45 | Vue.config.productionTip = false; 46 | 47 | const requireComponent = require.context( 48 | // The relative path of the components folder 49 | './components', 50 | // Whether or not to look in subfolders 51 | false, 52 | // The regular expression used to match base component filenames 53 | /Base[A-Z]\w+\.(vue|js)$/, 54 | ); 55 | 56 | requireComponent.keys().forEach((fileName) => { 57 | // Get component config 58 | const componentConfig = requireComponent(fileName); 59 | 60 | // Get PascalCase name of component 61 | const componentName = upperFirst( 62 | camelCase( 63 | // Gets the file name regardless of folder depth 64 | fileName 65 | .split('/') 66 | .pop() 67 | .replace(/\.\w+$/, ''), 68 | ), 69 | ); 70 | 71 | // Register component globally 72 | Vue.component( 73 | componentName, 74 | // Look for the component options on `.default`, which will 75 | // exist if the component was exported with `export default`, 76 | // otherwise fall back to module's root. 77 | componentConfig.default || componentConfig, 78 | ); 79 | }); 80 | 81 | Vue.component('vue-headful', vueHeadful); 82 | 83 | Vue.component('font-awesome-icon', FontAwesomeIcon); // Register component globally 84 | library.add( 85 | faHeartbeat, faFire, faLightbulb, faChevronLeft, faChevronRight, faChevronUp, faRss, faShareAlt, faTag, faBars, faPaperPlane, 86 | faGithub, faLinkedin, faTwitter, faReddit, faFacebook, faWhatsapp, faWeibo, faVk, 87 | ); // Include needed icons 88 | 89 | dom.i2svg(); 90 | 91 | Vue.use(Vuetify, { 92 | iconfont: 'faSvg', 93 | }); 94 | 95 | Vue.use(VueHighlightJS, { 96 | // Register only languages that you want 97 | languages: { 98 | javascript, 99 | python, 100 | dockerfile, 101 | go, 102 | shell, 103 | makefile, 104 | yaml, 105 | bash, 106 | ini, 107 | json, 108 | }, 109 | }); 110 | 111 | const SocialSharing = require('vue-social-sharing'); 112 | 113 | Vue.use(SocialSharing); 114 | 115 | Vue.use(VueMeta, { 116 | // optional pluginOptions 117 | refreshOnceOnNavigation: true, 118 | }); 119 | 120 | Vue.filter('formatDate', (value) => dayjs(String(value)).format('ll')); 121 | 122 | new Vue({ 123 | router, 124 | store, 125 | render: (h) => h(App), 126 | }).$mount('#app'); 127 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from '@/components/Home.vue'; 4 | import Blog from '@/components/Blog.vue'; 5 | import PostListPage from '@/components/PostListPage.vue'; 6 | import Contact from '@/components/Contact.vue'; 7 | import PostEditor from '@/components/PostEditor.vue'; 8 | import NotFound from '@/components/NotFound.vue'; 9 | import SubscribePage from '@/components/SubscribePage.vue'; 10 | 11 | Vue.use(Router); 12 | 13 | const router = new Router({ 14 | mode: 'history', 15 | routes: [ 16 | { path: '/', component: Home }, 17 | { path: '/blog/:id', component: Blog }, 18 | { path: '/posts/', component: PostListPage }, 19 | { path: '/contact', component: Contact }, 20 | { path: '/subscribe', component: SubscribePage }, 21 | { path: '/editor', component: PostEditor }, 22 | { path: '/404', component: NotFound }, 23 | { path: '*', redirect: '/404' }, 24 | ], 25 | }); 26 | 27 | export default router; 28 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import axios from 'axios'; 4 | import { API_URL } from '@/common/config'; 5 | 6 | /* eslint no-shadow: ["error", { "allow": ["state"] }] */ 7 | /* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }] */ 8 | 9 | Vue.use(Vuex); 10 | 11 | const types = { 12 | SET_BOOKS: 'SET_BOOKS', 13 | SET_PROJECTS: 'SET_PROJECTS', 14 | SET_POSTS: 'SET_POSTS', 15 | SET_CURRENT_POST: 'SET_CURRENT_POST', 16 | LOADING: 'LOADING', 17 | }; 18 | 19 | const state = { 20 | books: Object, 21 | projects: Object, 22 | posts: Array, 23 | currentPost: Object, 24 | nextPostExists: false, 25 | previousPostExists: false, 26 | refCount: 0, 27 | isLoading: false, 28 | }; 29 | 30 | const getters = { 31 | isLoading(state) { 32 | return state.isLoading; 33 | }, 34 | bookList(state) { 35 | return state.books; 36 | }, 37 | projects(state) { 38 | return state.projects; 39 | }, 40 | postList(state) { 41 | return state.posts; 42 | }, 43 | recentPostList(state) { 44 | if (Array.isArray(state.posts)) { 45 | return state.posts.slice(0, Math.min(state.posts.length, 10)); 46 | } 47 | return []; 48 | }, 49 | currentPost(state) { 50 | return state.currentPost; 51 | }, 52 | currentPostLDJson(state) { 53 | return { 54 | script: [{ 55 | type: 'application/ld+json', 56 | vmid: 'post-json-ld', 57 | json: { 58 | '@context': 'http://schema.org', 59 | '@type': 'BlogPosting', 60 | headline: state.currentPost.title, 61 | description: state.currentPost.title, 62 | keywords: '', // TODO Concatenate tags 63 | image: 'https://i.imgur.com/IUf6PIg.png', 64 | url: `https://${process.env.VUE_APP_API_URL}/blog/${state.currentPost.id}`, 65 | datePublished: state.currentPost.posted_on, 66 | dateModified: state.currentPost.posted_on, 67 | articleBody: state.currentPost.text, 68 | mainEntityOfPage: { 69 | '@type': 'WebPage', 70 | }, 71 | author: { 72 | '@type': 'Person', 73 | name: 'Martin Heinz', 74 | url: `https://${process.env.VUE_APP_API_URL}`, 75 | }, 76 | publisher: { 77 | '@type': 'Organization', 78 | name: 'Martin Heinz', 79 | url: `https://${process.env.VUE_APP_API_URL}`, 80 | logo: { 81 | '@type': 'ImageObject', 82 | url: 'https://i.imgur.com/IUf6PIg.png', 83 | width: '32', 84 | height: '32', 85 | }, 86 | }, 87 | }, 88 | }], 89 | }; 90 | }, 91 | 92 | currentPostText(state) { 93 | return { 94 | template: `
${state.currentPost.text}
`, 95 | }; 96 | }, 97 | currentPostHeader(state) { 98 | return { 99 | title: state.currentPost.title, 100 | author: state.currentPost.author, 101 | published: state.currentPost.posted_on, 102 | tags: state.currentPost.tags, 103 | }; 104 | }, 105 | previousPostId(state) { 106 | return state.currentPost.previous_post_id; 107 | }, 108 | nextPostId(state) { 109 | return state.currentPost.next_post_id; 110 | }, 111 | previousPostExists(state) { 112 | return state.previousPostExists; 113 | }, 114 | nextPostExists(state) { 115 | return state.nextPostExists; 116 | }, 117 | }; 118 | 119 | const actions = { 120 | fetchBooks({ commit }) { 121 | return axios.get(`${API_URL}books/`) 122 | .then((r) => r.data.books) 123 | .then((books) => { 124 | commit(types.SET_BOOKS, books); 125 | }); 126 | }, 127 | fetchProjects({ commit }) { 128 | return axios.get(`${API_URL}projects/`) 129 | .then((r) => r.data.projects) 130 | .then((projects) => { 131 | commit(types.SET_PROJECTS, projects); 132 | }); 133 | }, 134 | fetchPosts({ commit }) { 135 | return axios.get(`${API_URL}posts/`) 136 | .then((r) => r.data.posts) 137 | .then((posts) => { 138 | commit(types.SET_POSTS, posts); 139 | }); 140 | }, 141 | fetchPostById({ commit }, payload) { 142 | return axios.get(`${API_URL}posts/${payload.id}`) 143 | .then((r) => r.data) 144 | .then((data) => { 145 | commit(types.SET_CURRENT_POST, data); 146 | }); 147 | }, 148 | }; 149 | 150 | const mutations = { 151 | [types.SET_BOOKS](state, books) { 152 | state.books = books; 153 | }, 154 | [types.SET_PROJECTS](state, projects) { 155 | state.projects = projects; 156 | }, 157 | [types.SET_POSTS](state, posts) { 158 | state.posts = posts; 159 | }, 160 | [types.SET_CURRENT_POST](state, post) { 161 | state.currentPost = post; 162 | state.previousPostExists = post.previous_post_id !== 0; 163 | state.nextPostExists = post.next_post_id !== 0; 164 | }, 165 | [types.LOADING](state, isLoading) { 166 | if (isLoading) { 167 | state.refCount += 1; 168 | state.isLoading = true; 169 | } 170 | else if (state.refCount > 0) { 171 | state.refCount -= 1; 172 | state.isLoading = (state.refCount > 0); 173 | } 174 | }, 175 | }; 176 | 177 | const store = new Vuex.Store({ 178 | state, 179 | getters, 180 | actions, 181 | mutations, 182 | }); 183 | 184 | export const s = { 185 | state, 186 | getters, 187 | actions, 188 | mutations, 189 | }; 190 | 191 | export default store; 192 | -------------------------------------------------------------------------------- /startup.sh: -------------------------------------------------------------------------------- 1 | rm /etc/nginx/conf.d/default.conf 2 | # Start Nginx with special option in order to run in foreground 3 | if [ "$PROD" -eq 1 ]; then 4 | echo "Setting Nginx configuration for Production Environment..." 5 | sh -c "envsubst '\$NGINX_HOST' < /etc/nginx/conf.d/prod.template > /etc/nginx/conf.d/site.conf" 6 | fi 7 | 8 | nginx -g "daemon off;" 9 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress', 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true, 8 | }, 9 | rules: { 10 | strict: 'off', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/guides/guides/plugins-guide.html 2 | 3 | // if you need a custom webpack configuration you can uncomment the following import 4 | // and then use the `file:preprocessor` event 5 | // as explained in the cypress docs 6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 7 | 8 | /* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */ 9 | // const webpack = require('@cypress/webpack-preprocessor') 10 | 11 | module.exports = (on, config) => { 12 | // on('file:preprocessor', webpack({ 13 | // webpackOptions: require('@vue/cli-service/webpack.config'), 14 | // watchOptions: {} 15 | // })) 16 | 17 | return Object.assign({}, config, { 18 | fixturesFolder: 'tests/e2e/fixtures', 19 | integrationFolder: 'tests/e2e/specs', 20 | screenshotsFolder: 'tests/e2e/screenshots', 21 | videosFolder: 'tests/e2e/videos', 22 | supportFile: 'tests/e2e/support/index.js', 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/'); 6 | cy.contains('h1', 'Welcome to Your Vue.js App'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/baseFooter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaseFooter.vue renders correcly 1`] = ` 4 |
5 | 8 | 17 |
18 | `; 19 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/baseMenu.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaseMenu.vue renders correcly 1`] = ``; 4 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/bookList.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BookList.vue renders correcly 1`] = ` 4 |
5 |

Learning and Reading

6 |

These are books I read. My review and opinion about some of them can be found in blog posts.

7 |
The Go Programming LanguageClean Code: A Handbook of Agile Software Craftsmanship
8 |
9 | `; 10 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/menuActions.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MenuActions.vue renders correcly 1`] = ` 4 | 7 |