├── .gitattributes ├── static └── favicon.ico ├── layouts └── default.vue ├── pages ├── about │ └── index.vue ├── Lokis │ ├── index.vue │ └── _id.vue ├── Episodes │ ├── index.vue │ └── _id.vue └── index.vue ├── plugins └── vue-imgix.js ├── jsconfig.json ├── apollo └── queries │ ├── allHomes.gql │ ├── allEpisodes.gql │ ├── allHeroes.gql │ ├── oneEpisode.gql │ └── oneHero.gql ├── store └── README.md ├── vercel.json ├── package.json ├── components ├── Home.vue ├── Episodes.vue ├── Logo.vue ├── Heroes.vue ├── Header.vue ├── Hero.vue ├── Episode.vue ├── SocialHead.vue └── NavBar.vue ├── LICENSE ├── nuxt.config.js ├── .gitignore ├── tailwind.config.js └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/cosmicapp-nuxt-fan-site/main/static/favicon.ico -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/about/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /plugins/vue-imgix.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueImgix from 'vue-imgix'; 3 | 4 | Vue.use(VueImgix, { 5 | domain: "imgix.cosmicjs.com", 6 | defaultIxParams: { 7 | auto: 'format,compress', 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /apollo/queries/allHomes.gql: -------------------------------------------------------------------------------- 1 | { 2 | getObjects( 3 | bucket_slug: "", 4 | read_key: "", 5 | input: { 6 | query: { type: "homes" } 7 | } 8 | ) { 9 | objects { 10 | id 11 | title 12 | metadata 13 | slug 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apollo/queries/allEpisodes.gql: -------------------------------------------------------------------------------- 1 | { 2 | getObjects( 3 | bucket_slug: "", 4 | read_key: "", 5 | input: { 6 | query: { type: "episodes" } 7 | } 8 | ) { 9 | objects { 10 | id 11 | content 12 | title 13 | metadata 14 | slug 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apollo/queries/allHeroes.gql: -------------------------------------------------------------------------------- 1 | { 2 | getObjects( 3 | bucket_slug: "", 4 | read_key: "", 5 | input: { 6 | query: { type: "heroes" } 7 | } 8 | ) { 9 | objects { 10 | id 11 | content 12 | title 13 | metadata 14 | slug 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apollo/queries/oneEpisode.gql: -------------------------------------------------------------------------------- 1 | query getObjects($slug: String!){ 2 | getObjects( 3 | bucket_slug: "", 4 | read_key: "", 5 | input: { 6 | query: { slug: $slug } 7 | } 8 | ) { 9 | objects { 10 | id 11 | content 12 | title 13 | metadata 14 | slug 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apollo/queries/oneHero.gql: -------------------------------------------------------------------------------- 1 | query getObjects($slug: String!){ 2 | getObjects( 3 | bucket_slug: "", 4 | read_key: "", 5 | input: { 6 | query: { slug: $slug } 7 | } 8 | ) { 9 | objects { 10 | id 11 | content 12 | title 13 | metadata 14 | slug 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pages/Lokis/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /pages/Episodes/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /pages/Lokis/_id.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /pages/Episodes/_id.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "builds": [ 3 | { 4 | "src": "nuxt.config.js", 5 | "use": "@nuxtjs/vercel-builder", 6 | "config": {} 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/*", 12 | "headers": { 13 | "Accept-CH": "DPR, Width, Viewport-Width", 14 | "Feature-Policy": "ch-dpr https://imgix.cosmicjs.com 'self'; ch-width https://imgix.cosmicjs.com 'self'; ch-viewport-width https://imgix.cosmicjs.com 'self'" 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-nuxt-fan-site-template", 3 | "version": "1.0.0", 4 | "private": false, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate" 10 | }, 11 | "dependencies": { 12 | "@nuxtjs/apollo": "^4.0.1-rc.5", 13 | "@tailwindcss/typography": "^0.4.1", 14 | "core-js": "^3.15.1", 15 | "nuxt": "^2.15.7", 16 | "vue-imgix": "^2.9.0" 17 | }, 18 | "devDependencies": { 19 | "@nuxtjs/tailwindcss": "^4.2.0", 20 | "postcss": "^8.3.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /components/Home.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /components/Episodes.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /components/Logo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 21 | 33 | -------------------------------------------------------------------------------- /components/Heroes.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 daletom 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. -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /components/Hero.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /components/Episode.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Target: https://go.nuxtjs.dev/config-target 3 | target: 'static', 4 | 5 | // Global page headers: https://go.nuxtjs.dev/config-head 6 | head: { 7 | title: 'tom-template', 8 | htmlAttrs: { 9 | lang: 'en' 10 | }, 11 | meta: [ 12 | { charset: 'utf-8' }, 13 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 14 | { hid: 'description', name: 'description', content: '' }, 15 | { name: 'format-detection', content: 'telephone=no' } 16 | ], 17 | link: [ 18 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 19 | ] 20 | }, 21 | 22 | // Global CSS: https://go.nuxtjs.dev/config-css 23 | css: [ 24 | ], 25 | 26 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 27 | plugins: ['~/plugins/vue-imgix.js' 28 | ], 29 | 30 | // Auto import components: https://go.nuxtjs.dev/config-components 31 | components: true, 32 | 33 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 34 | buildModules: [ 35 | // https://go.nuxtjs.dev/tailwindcss 36 | '@nuxtjs/tailwindcss', 37 | ], 38 | 39 | // Modules: https://go.nuxtjs.dev/config-modules 40 | modules: ['@nuxtjs/apollo' 41 | ], 42 | 43 | apollo: { 44 | clientConfigs: { 45 | default: { 46 | httpEndpoint: `https://graphql.cosmicjs.com/v3`, 47 | } 48 | } 49 | }, 50 | 51 | // Build Configuration: https://go.nuxtjs.dev/config-build 52 | build: { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | .DS_Store -------------------------------------------------------------------------------- /components/SocialHead.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 75 | 76 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const plugin = require('tailwindcss/plugin') 2 | 3 | module.exports = { 4 | theme: { 5 | variants: { 6 | textColor: ['responsive', 'hover', 'focus', 'group-hover'], 7 | opacity: ['responsive', 'hover', 'focus', 'active', 'group-hover'], 8 | }, 9 | extend: { 10 | colors: { 11 | tokyosky: '#00012D', 12 | darkteal: '#041628', 13 | retropink: '#BA2D7E', 14 | lightblue: '#C6D0EB', 15 | darkblue: '#04142D', 16 | retroteal: '#6CE3D4', 17 | neonhaze: '#0E4985', 18 | infoblue: '#2F40EB', 19 | retrogreen: '#80D34D', 20 | retroorange: '#fd7817', 21 | retroyellow: '#fed318', 22 | retrored: '#fd3458', 23 | dangerred: '#F1114D', 24 | darkpurple: '#200C53', 25 | duststorm: '#F6EAD0', 26 | tomblue: '#214B91', 27 | tomred: '#BF1E2F', 28 | tomorange: '#F25827' 29 | }, 30 | inset: { 31 | '16': '4rem', 32 | }, 33 | }, 34 | fontFamily: { 35 | 'h1' : ['"Uni Neue W05 Black"','serif'], 36 | 'h2' : ['"Uni Neue W05 Bold"','serif'], 37 | 'body' : ['"Uni Neue W05 Regular"', 'serif'] 38 | }, 39 | typography: { 40 | default: { 41 | css: { 42 | color: '#C6D0EB', 43 | h1: { 44 | color: '#BA2D7E', 45 | }, 46 | h2: { 47 | color: '#6CE3D4', 48 | }, 49 | h3: { 50 | color: '#C6D0EB', 51 | }, 52 | blockquote: { 53 | color: '#fed318' 54 | }, 55 | strong: { 56 | color: '#BA2D7E', 57 | }, 58 | a: { 59 | color: '#2F40EB', 60 | '&:hover': { 61 | color: '#2F40EB', 62 | }, 63 | }, 64 | aspectRatio: { 65 | none: 0, 66 | square: [1, 1], 67 | "16/9": [16, 9], 68 | "4/3": [4, 3], 69 | "21/9": [21, 9] 70 | }, 71 | screens: { 72 | xs: '320px', 73 | sm: '640px', 74 | md: '768px', 75 | lg: '1024px', 76 | xl: '1280px', 77 | xxlmin: '1351px', 78 | xxlmax: { max: '1350px' } 79 | }, 80 | code: { 81 | color: '#f7fafc', 82 | fontWeight: '400', 83 | fontSize: '.875em', 84 | backgroundColor: '#2d3748', 85 | padding: '.25rem', 86 | borderWidth: '0', 87 | borderColor: '#edf2f7', 88 | borderRadius: '.25rem', 89 | } 90 | }, 91 | }, 92 | dark: { 93 | css: { 94 | color: '#C6D0EB', 95 | h1: { 96 | color: '#BA2D7E', 97 | }, 98 | h2: { 99 | color: '#6CE3D4', 100 | }, 101 | h3: { 102 | color: '#C6D0EB', 103 | }, 104 | blockquote: { 105 | color: '#fed318' 106 | }, 107 | strong: { 108 | color: '#BA2D7E', 109 | }, 110 | a: { 111 | color: '#80D34D', 112 | '&:hover': { 113 | color: '#fed318', 114 | }, 115 | }, 116 | code: { 117 | color: '#f7fafc', 118 | fontWeight: '400', 119 | fontSize: '.875em', 120 | backgroundColor: '#2d3748', 121 | padding: '.25rem', 122 | borderWidth: '0', 123 | borderColor: '#edf2f7', 124 | borderRadius: '.25rem', 125 | } 126 | }, 127 | } 128 | }, 129 | }, 130 | plugins: [ 131 | require('@tailwindcss/typography'), 132 | plugin(function({ addUtilities }) { 133 | const newUtilities = { 134 | '.teal-glow': { 135 | textShadow: '0px 0px 8px rgba(108,227,212,0.6)', 136 | }, 137 | '.pink-glow': { 138 | textShadow: '0px 0px 8px rgba(186,45,126,0.6);', 139 | }, 140 | '.yellow-glow': { 141 | textShadow: '0px 0px 8px rgba(254,211,24,0.6)', 142 | }, 143 | '.green-glow': { 144 | textShadow :'0px 0px 8px rgba(128,211,77,0.6)', 145 | }, 146 | '.white-glow': { 147 | textShadow :'0px 0px 8px rgba(255,255,255,0.6)', 148 | }, 149 | '.light-blue-glow': { 150 | textShadow : '0px 0px 8px rgba(198, 208, 235, 0.6)', 151 | }, 152 | '.red-glow': { 153 | textShadow : '0px 0px 8px rgba(253, 52, 88, 0.6)', 154 | }, 155 | '.orange-glow': { 156 | textShadow: '0px 0px 8px rgba(253, 120, 23,0.6)', 157 | } 158 | } 159 | 160 | addUtilities(newUtilities, ['responsive', 'hover']) 161 | }) 162 | ], 163 | purge: { 164 | enabled: process.env.NODE_ENV === 'production', 165 | content: [ 166 | 'components/**/*.vue', 167 | 'layouts/**/*.vue', 168 | 'pages/**/*.vue', 169 | 'plugins/**/*.js', 170 | 'nuxt.config.js', 171 | 'content/**/*.md' 172 | ], 173 | options: { 174 | whitelist: [ 175 | 'border-retroyellow', 176 | 'border-retroteal', 177 | 'border-retrogreen', 178 | 'border-infoblue', 179 | 'border-retrored', 180 | 'text-retroyellow', 181 | 'text-retrored', 182 | 'text-retrogreen', 183 | 'text-retroteal', 184 | 'text-infoblue', 185 | 'text-retropink' 186 | ], 187 | } 188 | }, 189 | experimental: { 190 | darkModeVariant: true 191 | }, 192 | dark: 'class', 193 | important: true, 194 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Fan Site 2 | This is a Nuxt App built using Cosmic as its Headless CMS. It utilizes Apollo to access the [Cosmic](https://www.cosmicjs.com) GraphQL API. It uses Tailwind CSS as the CSS framework. It utilizes Cosmic's built in integration with [imgix](https://imgix.com) to build out responsive images as well as advanced use cases for og image optimization, and blending with text. 3 | 4 | There is a [video tutorial](https://youtu.be/YYMptR7Fpn4) which was done by Tony Spiro (CEO at Cosmic) and Tom Dale (Head of Customer Success at imgix). There is also an accompanying [App in the Cosmic Marketplace](https://www.cosmicjs.com/apps/nuxt-fan-site-with-responsive-images) and [live demo](https://cosmicapp-nuxt-fan-site.vercel.app/). 5 | 6 | ## Getting Started 7 | Take the following steps to get your fan site up and running locally connected to Cosmic: 8 | 1. [Log in to Cosmic](https://app.cosmicjs.com/login) and install the [Nuxt Fan Site](https://www.cosmicjs.com/apps/nuxt-fan-site-with-responsive-images) application which includes demo content. 9 | 2. Clone this repository 10 | ``` 11 | git clone https://github.com/cosmicjs/cosmicapp-nuxt-fan-site 12 | ``` 13 | 3. Add your `bucket_slug` and `read_key` info to the files in the folder `apollo/queries`. You can find these in your Cosmic dashboard in _Bucket Settings > API Access_. 14 | 4. Using the terminal, cd into your app, install dependancies, and start your app. 15 | ``` 16 | cd cosmicapp-nuxt-fan-site 17 | yarnr 18 | yarn dev 19 | ``` 20 | Your fan site should now be running at `http://localhost:3000`! Go into your Cosmic dashboard and edit content to see it reflected on your application. Follow the steps below to add content from scratch and learn more about this application. 21 | 22 | ## Adding Content to Cosmic 23 | 24 | You are going to first want to add some content to Cosmic for your fan site or whatever site you are making. You will see several buckets in Cosmic to add content to. Let's start with the homes bucket, for your home page. 25 | 26 | 27 | 28 | Each item you add in the homes bucket will reflect a different section you can click into on your home page. It's important to think about the organization of your site now, because this is what will enable that. In the example, I make a Loki site and created three sections: 29 | 30 | * Episodes (a section to look at each episode from the show) 31 | * Lokis (a section to look at each Loki variant from the show) 32 | * About (an about page) 33 | 34 | These sections have very limited info needed. Just a title and an image. Their names are important, because this will become the routes for each section in your nuxt app that you will be updating. 35 | 36 | 37 | 38 | You will now want to create Cosmic buckets for each section you made. In the template, you already have the three I created: 39 | 40 | * episodes 41 | * heroes 42 | * homes 43 | 44 | You may want to edit or add additional ones. These buckets will allow you to add content to each of those sections you started creating. 45 | 46 | When you create a cosmic bucket, select them as a multiple bucket at the top. This is because you are going to enter multiple entries. 47 | 48 | 49 | 50 | Then leave the default settings of title, slug, content alone. You will then most likely want to add an image to the metadata area and anything else you may want to display. In my examples, I added a heroimage and an episode_number. 51 | 52 | 53 | 54 | You can go ahead and finish making these buckets and add some content to them. 55 | 56 | ## Apollo GraphQL Queries with Cosmic 57 | 58 | Once you have some content in your Cosmic buckets, you now need to make a few updates to the Nuxt app in order to display your content. You will need to update the GraphQL Queries we are doing to Cosmic. For these queries, I am using Apollo. You will find 5 Queries in a apollo/queries folder. 59 | 60 | 61 | 62 | Here is an example of what one of the queries looks like: 63 | 64 | ``` 65 | { 66 | getObjects( 67 | bucket_slug: "", 68 | read_key: "", 69 | input: { 70 | query: { type: "episodes" } 71 | } 72 | ) { 73 | objects { 74 | id 75 | content 76 | title 77 | metadata 78 | slug 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | You will need to update the bucket_slug and the read_key. Also, if you are adding additional or Modifying Cosmic Buckets, you would add the name of the bucket to the query type. You can find the bucket_slug and read_key in your Cosmic settings, API Access. We are not using the write_key, so the rest of these items are not a concern to publicly be seen. That's why they are not obscured in my screenshot or added to .env files or anything. 85 | 86 | 87 | 88 | If you do want to do a bit of a deeper dive on the GraphQL, Cosmic does have a simple GraphQL playground. Just click on one of your Cosmic Buckets, then next to the name of it, you will see Developer Tools. This can help you debug items and see what the values you are going to get from each query. The reason I am choosing to use the GraphQL is to ensure that I don't get large payloads by accessing all of the content of my Cosmic site on each page load. The GraphQL allows me to get pretty specific and only ask for certain content. In my example query above, I'm only asking for the id, content, title, slug, and metadata of the items in the episodes section. The metadata is the additional items we added, like the hero image or the episode number. 89 | 90 | 91 | 92 | Once you have filled in the bucket_slug and the read_key, your queries should look like this: 93 | 94 | ``` 95 | { 96 | getObjects( 97 | bucket_slug: "loki-webinar-production", 98 | read_key: "TMNjyVSNbfE75hA7q92OTZex74Ye5NuIn20DRWNroYgRGj4y9Y", 99 | input: { 100 | query: { 101 | type: "episodes" 102 | } 103 | }) { 104 | objects { 105 | id 106 | content 107 | title 108 | metadata 109 | slug 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | You can also certainly use my bucket_slug and read_key if you would like to just see the content I did in my demo app. 116 | 117 | ## Deploy Via Vercel 118 | 119 | At this point, you actually don't need to make any additional changes. You can run the app locally using `yarn dev` or deploy it easily via Vercel or whatever tool you prefer. There are certainly additional tweeks we can make to the site which I will go over below if you would like as well. I have added a vercel.json file to this template, so it is extremely simple to deploy the site. I find it easiest to push the site to Github and connect your Github with Vercel. You can then just click `New Project` in Vercel and choose the git repo you pushed the project to. No other adjustments or settings need to be added. The settings of the vercel.json ensures everything will work smoothly. 120 | 121 | ## Customizations to Routes 122 | 123 | The routes are set-up for three paths in the pages folder of your nuxt-app. They are set up as: 124 | 125 | * Episodes 126 | * Lokis 127 | * About 128 | 129 | Depending on how many objects you created in your homes bucket and what they are called, you should modify these. In my example, I called them Lokis, Episodes, and About in my Cosmic bucket, which is why I changed the names of the folders in my pages section to be identical to those. 130 | 131 | If you open one of those folders (I opened episodes) you will see a index.vue and a _id.vue. The _id.vue is a dynamic route, for each of the individual objects I created in my episodes bucket, there will be a dynamic route to match those. No customization needs to happen with the _id.vue. 132 | 133 | For the index.vue, this would be the episodes page, which displays all of the objects you added to the episodes bucket. In the index.vue of the episodes folder, you will notice it is pretty empty. 134 | 135 | ``` 136 | 141 | 142 | 154 | ``` 155 | 156 | In the template, I am simply injecting a component called Episodes.vue. In the script, I am referencing one of my GraphQL queries from Apollo. This is how I prefetch the data from that query to display the content in the Episodes component. Let's have a look in the components folder at the Episodes.vue component. 157 | 158 | ``` 159 | 179 | ``` 180 | 181 | I am using Vue's `v-for` functionality to loop through each of the results from the episodes bucket we made in Cosmic. For each result, it will display an image and the content. It is wrapped in the `nuxt-link` which is a simple nuxt component that pre-generates a route so it loads instantly when you click it. Now if you created a different object in your homes that wasn't called episodes, you would want to modify one item here. 182 | 183 | ``` 184 | 185 | ``` 186 | 187 | The route is going to something called `/episodes/`. But if you changed the name of the folder in pages, then you will want this to match that. That way if you click on an object, it will go to that same route. 188 | 189 | For the image, I am using a Vue SDK from imgix to generate a responsive design which is already installed in the template. That is why it says `ix-img` instead of `img`. I am calling the heroimage that we created in the metadata of our bucket in cosmic by referencing this `article.metadata.heroimage.imgix_url`. This isn't anything you need to change, as long as you also used the template bucket with the heroimage title for your images. If you created something customer yourself, you would just need to change the heroimage part. These images are going to be displayed at 500 x 281, which is determind by the height and width I added. I chose for these images to be fixed. The Vue SDK will create a srcset for each image that will create several different sizes to intelligently matcth the device pixel ratio of a user. This is nothing you need to change or worry about. If you want different sizes, then just change the width and height here. The loading lazy is also the native lazy loading for Chrome web browsers, again, nothing you need to change. 190 | 191 | ## The ix-img from imgix 192 | 193 | I am actually using the [Vue SDK](https://github.com/imgix/vue-imgix) from imgix. I have installed it already in this repo. You can see the settings for it in my plugins folder: 194 | 195 | ``` 196 | import Vue from 'vue'; 197 | import VueImgix from 'vue-imgix'; 198 | 199 | Vue.use(VueImgix, { 200 | domain: "imgix.cosmicjs.com", 201 | defaultIxParams: { 202 | auto: 'format,compress', 203 | }, 204 | }) 205 | ``` 206 | 207 | There is nothing needed to be changed here, but simply showing the idea that I am importing this Vue plugin into Nuxt. I am declaring what imgix url to use, which is the url provided by Cosmic in this instance. To finish registering this plugin, you will want to go into the nuxt.config.js and add it to the plugins section as well. 208 | 209 | ``` 210 | plugins: ['~/plugins/vue-imgix.js' 211 | ], 212 | ``` 213 | 214 | Now that the plugin is set up, you can use `` instead of `` tags. Now you have the option to generate various types of responsive images with srcsets as well as easily adding imgix parameters. From one example in my Episodes.vue Component, you can see me creating a fixed image. I have declared the width and heigh and then added fixed. 215 | 216 | ``` 217 | 224 | ``` 225 | 226 | By doing this, it will generate a srcset that creates multiple versions of that same size of image with different device pixel ratios. The browser can then choose the correct DPR image based on the device viewing the image. It's quite important to display higher dpr images to certain devices, like mobile phones, otherwise they will end up looking blurry. Higher DPR images also have denser pixel data, so they can withstand higher levels of compression without visually impairing the image. That's why in the generated srcsets you can notice that the quality value decreases as the dpr value increases. This is a great way of serving great looking performant images. Here is an example of a srcset that would be generated: 227 | 228 | ``` 229 | 235 | ``` 236 | 237 | All I had to do was install the Vue SDK and change the img tag to an ix-img, a very easy way to generate great srcsets for my images. 238 | 239 | ## Optimizing my OG images 240 | 241 | Something you always want users to do is to share content from your website. This is a great opportunity for your content to be viewed by many other users that may have not found it. I love it when shared content is customized in some way to stand out. One way I love to do this is by optimizing the og:image and twitter:image. You can add watermarks or logos of your website to these images, maybe you can even overlay text on a nice overlay from your article on the image. 242 | 243 | That's going to be a lot of time in figma or photoshop to create all these customized images to be shared. But wait, all of the images are on imgix. imgix is practically a photoshop on the fly, with tons of APIs to add custom watermarks, blends, text, etc. I can actually create a nice function or component in Nuxt to programmatically append the metadata to each of the og:images for every article. If you go to the episode.vue component, you will see a component I reference their called ``. I got this idea from a Nuxt tutorial. All this component is doing is passing the title, content, and image url from Cosmic to the Social head component. 244 | 245 | ``` 246 | 251 | ``` 252 | 253 | Now if you go to the actual SocialHead.vue component file, you will see that it is generated the head metadata. it will add the title and content to the appropriate og and twitter fields for each article. I am also adding some further imgix APIs to the og:image and twitter:image. For the twitter card, I want a 450 x 450 image. I am including the website logo in the bottom right as well as a watermark. Then I am also adding a dark gradient to the image, so I can overlay the name of the article as text on the image. 254 | 255 | ``` 256 | { 257 | hid: 'twitter:image', 258 | name: 'twitter:image', 259 | content: this.image + "?w=450&ar=1:1&fit=crop&crop=faces,edges&blend-size=inherit&blend-mode=multiply&blend=https://demos.imgix.net/dark-ellipse-gradient.ai?fm=png%20w=450&txt-align=middle,center&txt-size=72&mark64=aHR0cHM6Ly9pbWdpeC5jb3NtaWNqcy5jb20vYmViZDVlZDAtZjcyZS0xMWViLWExNzEtNzFkOWRiMGVlNDk2LWxva2lsb2dvLmdpZj93PTMwMCZoPTEyMCZmaXQ9Y3JvcCZmcmFtZT02&txt-fit=max&txt-pad=20&txt-color=white&txt=" + this.title 260 | }, 261 | ``` 262 | 263 | Once you publish your app, you can test the pages to see their social share content is working. I like to use the [Twitter Validator](https://cards-dev.twitter.com/validator) to do this. If you enter a url from my demo site, like this one:https://cosmicapp-nuxt-fan-site.vercel.app/episodes/journey-into-mystery, you will then see the twitter card I created: 264 | 265 | 266 | 267 | 268 | ## Conclusion 269 | 270 | Not only has this been a really fun site template to make, it's been great to further explore some advanced ways to use imgix to optimize the images. it is truly a powerful tool to have already included with your Cosmic accounts. Remember, there is a [tutorial video](https://youtu.be/YYMptR7Fpn4) which was done by Tony Spiro (CEO at Cosmic) and Tom Dale (Head of Customer Success at imgix). There is also an accompanying App in the [Cosmic Marketplace](https://www.cosmicjs.com/apps/nuxt-fan-site-with-responsive-images). I hope you do fun things with this app and my suggestions and look forward to helping with anything else you might need. 271 | --------------------------------------------------------------------------------