Start typing to search...
Docs
Blueprints
Magazine cover template
Search

Magazine cover template

Follow this blueprint to build a template for your own picture and headline on a magazine cover. You will add customizable images and text, and then publish the template on Koji.

Make a magazine cover template on Koji

Prerequisites

  • Familiarity with web development. React and ES6 basics are a plus.

  • Familiarity with the Koji editor and remix process. For an overview, go through the Koji quick-start tutorial.

Level

  • Koji: Beginner – Intermediate

  • Developer: Intermediate – Advanced

  • Time: 45 minutes

Building blocks

Remix the scaffold

Remix an existing web application on Koji that implements basic elements of your favorite framework.

React
Vanilla JS

Install the packages

Install @withkoji/vcc to expose Visual Customization Controls (VCCs), dynamically update custom values, and display your template correctly in the Koji feed.

npm install --save @withkoji/vcc
Note
To use this package, you must configure the package.json file to run the watcher (koji-vcc watch) concurrently with your development server.

Create the customization files

Create .koji/customization/settings.json to define customizable values for the magazine name, cover image, background color, and text options (text, size, color, and position).

{
  "settings": {
    "magazineOptions": {
      "magazineName": "https://images.koji-cdn.com/38587b11-db1e-4e64-b099-a63e22f3666c/nfakd-2.png",
      "coverImage": "https://images.koji-cdn.com/14c02947-0b36-4b2b-ace3-6d205bb84e3b/tl7ta-download20200602T105158.824.png",
      "bgColor": "#f7e8a2"
    },
    "textOptions": {
      "title": "The greatest ever...",
      "fontSize": 15,
      "color": "#592efb",
      "position": {
        "x": 2,
        "y": 30
      }
    },
    "background": "#be47f9"
  },
  "@@editor": [
    {
      "key": "settings",
      "name": "App settings",
      "icon": "⚙️",
      "source": "settings.json",
      "fields": [
        {
          "key": "magazineOptions",
          "name": "Magazine options",
          "type": "object<MagazineOption>",
          "typeOptions": {
            "MagazineOption": {
              "magazineName": {
                "name": "Magazine name",
                "type": "select",
                "typeOptions": {
                  "placeholder": "Choose a magazine...",
                  "options": [
                    {
                      "value": "https://images.koji-cdn.com/5eaaf00f-c8bb-46ba-8de2-96c41a44d93a/w44bz-Cosmofix.png",
                      "label": "Cosmopolitan"
                    },
                    {
                      "value": "https://images.koji-cdn.com/48a8b044-5695-4bf0-8726-6f6d1a09b69e/usir2-BrowserPreview_tmp.png",
                      "label": "Forbes"
                    },
                    {
                      "value": "https://images.koji-cdn.com/23ac8d20-2619-43ce-80f9-0e085e72dc8d/yznns-01.png",
                      "label": "AARP"
                    },
                    {
                      "value": "https://images.koji-cdn.com/38587b11-db1e-4e64-b099-a63e22f3666c/nfakd-2.png",
                      "label": "GQ"
                    }
                  ]
                }
              },
              "coverImage": {
                "name": "Cover image",
                "type": "image"
              },
              "bgColor": {
                "name": "Background Color",
                "type": "color"
              }
            }
          }
        },
        {
          "key": "textOptions",
          "name": "Text options",
          "type": "object<TextOption>",
          "typeOptions": {
            "TextOption": {
              "title": {
                "name": "Headline",
                "description": "Headline for your cover image",
                "type": "text"
              },
              "fontSize": {
                "name": "Text size",
                "description": "Select a size for the title font",
                "type": "range",
                "typeOptions": {
                  "min": 4,
                  "max": 30,
                  "step": 1
                }
              },
              "color": {
                "name": "Color",
                "type": "color"
              },
              "position": {
                "name": "Text position",
                "type": "coordinate"
              }
            }
          }
        }
      ]
    }
  ]
}

Remove the unused customization files, if applicable.

React
Example 1. React

colors.json, images.json, and strings.json

Add the template logic

Import the packages.

import { FeedSdk, InstantRemixing } from '@withkoji/vcc';

Use InstantRemixing to get and set custom values.

const instantRemixing = new InstantRemixing();

Use FeedSdk to display the template in the Koji feed.

const feed = new FeedSdk();

Monitor the remix state.

React
Vanilla JS
React
  const [isRemixing, setIsRemixing] = useState(instantRemixing.isRemixing);
  useEffect(() => {
    instantRemixing.onSetRemixing((isNowRemixing) => {
      setIsRemixing(isNowRemixing);
    });
  }, []);
Vanilla JS
var isRemixing = instantRemixing.isRemixing;
instantRemixing.onSetRemixing(isNowRemixing => {
    isRemixing = isNowRemixing;
    render();
});

Dynamically get and set custom values.

React
Vanilla JS
React
  const [magazineOptions, setMagazineOptions] = useState(instantRemixing.get(['settings', 'magazineOptions']));
  const [textOptions, setTextOptions] = useState(instantRemixing.get(['settings', 'textOptions']));

  useEffect(() => {
    instantRemixing.onValueChanged(([scope = '', key = ''], value) => {
      if (scope === 'settings' && key === 'magazineOptions') setMagazineOptions(value);
      if (scope === 'settings' && key === 'textOptions') setTextOptions(value);
    });
  }, []);
Vanilla JS
var magazineOptions = instantRemixing.get(['settings', 'magazineOptions']);
var textOptions = instantRemixing.get(['settings', 'textOptions']);

// Set values on remix
instantRemixing.onValueChanged(([scope = "", key = ""], value) => {
    if (scope === 'settings' && key === 'magazineOptions') magazineOptions = value;
    if (scope === "settings" && key === "textOptions") textOptions = value;
    render();
});

Optimize the user’s cover image.

const optimizeURL = (url) => `${url}?fit=bounds&width=${window.innerWidth - 15}&height=${window.innerHeight - 15}&optimize=medium`;

Add click handlers for editable elements.

  // Click handlers for remixing
  const handleClick = (e) => {
    if (isRemixing) {
      if (e.target.closest('.editable')) {
        instantRemixing.onPresentControl(['settings', 'textOptions']);
      } else {
        instantRemixing.onPresentControl(['settings', 'magazineOptions']);
      }
    }
  };

Use dynamic sizing to support different browsers and devices.

React
Vanilla JS
React
  // Handle dynamic resizing
  const [size, setSize] = useState({});

  // Set a reference for the magazine div (import useRef from React to follow this pattern)
  const magazineRef = useRef(null);

  // Set dynamic size properties for the cover image
  const runSetSize = () => {
    setSize({
      height: magazineRef.current.height,
      width: magazineRef.current.width,
      top: magazineRef.current.offsetTop,
      left: magazineRef.current.offsetLeft,
    });
  };

  // Re-run the set sizing function when the window is resized
  useEffect(() => {
    window.addEventListener('resize', runSetSize);
    return () => window.removeEventListener('resize', runSetSize);
  }, []);
Vanilla JS
// Handle dynamic resizing
var size = {};
const setSize = () => {
    let magazine = document.getElementById('magazine');
    if (magazine) {
        let oldSize = size;
        size = {
            height: magazine.height,
            width: magazine.width,
            top: magazine.offsetTop,
            left: magazine.offsetLeft
        };
        if (oldSize.height !== size.height || oldSize.width !== size.width || oldSize.top !== size.top || oldSize.left !== size.left) {
            render();
        }
    }
}
window.addEventListener("resize", setSize);

Indicate the template is ready for instant remixes and to display in the feed.

React
Vanilla JS
React
  useEffect(() => {
    // Wrap in the final useEffect so it's sure to run after anything else
    instantRemixing.ready();
    feed.load();
  }, []);
Vanilla JS
instantRemixing.ready();
feed.load();

Render the template with custom values. Apply conditional styling to editable elements during remix.

React
Vanilla JS
React
    <Wrapper
      onClick={handleClick}
      style={{
        bgColor: magazineOptions.bgColor,
      }}
    >
      <Magazine
        onLoad={runSetSize}
        ref={magazineRef}
        src={magazineOptions.magazineName}
      />
      {
            size.height &&
            <MagazineCover
              style={{
                ...size,
                backgroundImage: optimizeURL(magazineOptions.coverImage),
              }}
            >
              <H1
                className={isRemixing ? 'editable text' : 'text'}
                fontSize={size.height ? (textOptions.fontSize / 200) * size.height : 20}
                color={textOptions.color}
                x={textOptions.position.x}
                y={textOptions.position.y}
              >
                {textOptions.title}
              </H1>
            </MagazineCover>
        }
    </Wrapper>
Vanilla JS
// render app
const render = () => {
    let coverStyles = `
        width: ${size.width}px;
        height: ${size.height}px;
        top: ${size.top}px;
        left: ${size.left}px;
        background-image: url(${optimizeURL(magazineOptions.coverImage)});
    `;
    let titleStyles = `
        font-size: ${size.height ? (textOptions.fontSize / 200) * size.height : 20}px;
        color: ${textOptions.color};
        top: ${textOptions.position.y}%;
        left: ${textOptions.position.x}%;
    `;
    document.body.innerHTML = `
        <div id="wrapper">
            <img id="magazine" src="${magazineOptions.magazineName}"/>
            <div id="magazineCover" style="${coverStyles}">
                <div id="title" class="${isRemixing ? 'editable ': ''}text" style="${titleStyles}">
                    ${textOptions.title}
                </div>
            </div>
        </div>
    `;
    document.getElementById('wrapper').addEventListener('click', handleClick);
    document.getElementById('magazine').onload = setSize;
};

// render
render();

Add the styles

Define styles for the template, including the headline, cover image. Remove unused styles.

React
Vanilla JS
React
const Wrapper = styled.div`
  background-color: ${({ style: { bgColor } }) => bgColor};
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const MagazineCover = styled.div`
  width: ${({ style: { width } }) => width}px;
  height: ${({ style: { height } }) => height}px;
  top: ${({ style: { top } }) => top}px;
  left: ${({ style: { left } }) => left}px;
  background: url("${({ style: { backgroundImage } }) => backgroundImage}") no-repeat center center / cover;
  position: absolute;
  z-index: 0;
`;

const Magazine = styled.img`
  max-height: 100%;
  max-width: 100%;
  z-index: 1;
  pointer-events: none;
`;

const H1 = styled.div`
  position: absolute;
  top: ${({ y }) => `${y}%`};
  left: ${({ x }) => `${x}%`};
  font-family: 'Roboto', sans-serif;
  color: ${({ color }) => color};
  font-size: ${({ fontSize }) => `${fontSize}px`};
  max-width: 35%;
  cursor: default;
`;
Vanilla JS
div#wrapper {
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

div#magazineCover {
  background: no-repeat center center / cover;
  position: absolute;
  z-index: 0;
}

img#magazine {
  max-height: 100%;
  max-width: 100%;
  z-index: 1;
  pointer-events: none;
}

div#title {
  position: absolute;
  font-family: "Roboto", sans-serif;
  max-width: 35%;
  cursor: default;
}

.editable {
  border: 2px dashed grey;
}

Add styles for editable elements during remix.

.editable {
  border: 2px dashed grey;
}

Add the font stylesheet.

<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">

Add entitlements

Create .koji/project/entitlements.json and enable instant remix support and listing in the Koji feed.

{
  "entitlements": {
    "InstantRemixing": true,
    "FeedEvents": true
  }
}

Test

Use the tools in the Koji editor to test template functionality. For example:

  • Template default view – Refresh the Live preview tab.

  • Conditional styling of editable elements – In the live preview, switch between Preview and Editing mode.

  • VCC targeting – In Editing mode, click each editable element. The corresponding VCC should open.

  • Remix functionality – In the JSON file, switch to Visual view, and use VCC editor to customize values. The template should update immediately.

  • Template styles in another browser tab or device – To open a preview in a new browser tab, click the remote staging button Remote staging button in the preview pane. To open a preview on a mobile device, click the QR code button QR code button in the preview pane to display a QR code, then scan the QR code with your mobile device.

Publish

Click Publish Now and enter a Name (defines the permalink URL to your template), Description (displays along with your template on Koji), and other publish settings, as desired. Then, click Publish New Version.

When publishing is completed, click the link in the message to view your live template on Koji. Your template is now available to remix and share anywhere on the web. You can create a fun version for yourself and share it on your favorite site to see what your friends make.

"As Built" sample code

To see this blueprint as a completed template on Koji, visit the following link. From there, you can view the source code or remix the template into your own project.

React
Vanilla JS

The following code is a completed sample of the template logic described in this blueprint.

React
Vanilla JS
React
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { FeedSdk, InstantRemixing } from '@withkoji/vcc';

const Wrapper = styled.div`
  background-color: ${({ style: { bgColor } }) => bgColor};
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const MagazineCover = styled.div`
  width: ${({ style: { width } }) => width}px;
  height: ${({ style: { height } }) => height}px;
  top: ${({ style: { top } }) => top}px;
  left: ${({ style: { left } }) => left}px;
  background: url("${({ style: { backgroundImage } }) => backgroundImage}") no-repeat center center / cover;
  position: absolute;
  z-index: 0;
`;

const Magazine = styled.img`
  max-height: 100%;
  max-width: 100%;
  z-index: 1;
  pointer-events: none;
`;

const H1 = styled.div`
  position: absolute;
  top: ${({ y }) => `${y}%`};
  left: ${({ x }) => `${x}%`};
  font-family: 'Roboto', sans-serif;
  color: ${({ color }) => color};
  font-size: ${({ fontSize }) => `${fontSize}px`};
  max-width: 35%;
  cursor: default;
`;

const optimizeURL = (url) => `${url}?fit=bounds&width=${window.innerWidth - 15}&height=${window.innerHeight - 15}&optimize=medium`;

const instantRemixing = new InstantRemixing();
const feed = new FeedSdk();

const App = () => {
  // Handle remixing state
  const [isRemixing, setIsRemixing] = useState(instantRemixing.isRemixing);
  useEffect(() => {
    instantRemixing.onSetRemixing((isNowRemixing) => {
      setIsRemixing(isNowRemixing);
    });
  }, []);

  // Handle value updates
  const [magazineOptions, setMagazineOptions] = useState(instantRemixing.get(['settings', 'magazineOptions']));
  const [textOptions, setTextOptions] = useState(instantRemixing.get(['settings', 'textOptions']));

  useEffect(() => {
    instantRemixing.onValueChanged(([scope = '', key = ''], value) => {
      if (scope === 'settings' && key === 'magazineOptions') setMagazineOptions(value);
      if (scope === 'settings' && key === 'textOptions') setTextOptions(value);
    });
  }, []);

  // Click handlers for remixing
  // Click handlers for remixing
  const handleClick = (e) => {
    if (isRemixing) {
      if (e.target.closest('.editable')) {
        instantRemixing.onPresentControl(['settings', 'textOptions']);
      } else {
        instantRemixing.onPresentControl(['settings', 'magazineOptions']);
      }
    }
  };

  // Handle dynamic resizing
  const [size, setSize] = useState({});

  // Set a reference for the magazine div (import useRef from React to follow this pattern)
  const magazineRef = useRef(null);

  // Set dynamic size properties for the cover image
  const runSetSize = () => {
    setSize({
      height: magazineRef.current.height,
      width: magazineRef.current.width,
      top: magazineRef.current.offsetTop,
      left: magazineRef.current.offsetLeft,
    });
  };

  // Re-run the set sizing function when the window is resized
  useEffect(() => {
    window.addEventListener('resize', runSetSize);
    return () => window.removeEventListener('resize', runSetSize);
  }, []);

  useEffect(() => {
    // Wrap in the final useEffect so it's sure to run after anything else
    instantRemixing.ready();
    feed.load();
  }, []);

  return (
    <Wrapper
      onClick={handleClick}
      style={{
        bgColor: magazineOptions.bgColor,
      }}
    >
      <Magazine
        onLoad={runSetSize}
        ref={magazineRef}
        src={magazineOptions.magazineName}
      />
      {
            size.height &&
            <MagazineCover
              style={{
                ...size,
                backgroundImage: optimizeURL(magazineOptions.coverImage),
              }}
            >
              <H1
                className={isRemixing ? 'editable text' : 'text'}
                fontSize={size.height ? (textOptions.fontSize / 200) * size.height : 20}
                color={textOptions.color}
                x={textOptions.position.x}
                y={textOptions.position.y}
              >
                {textOptions.title}
              </H1>
            </MagazineCover>
        }
    </Wrapper>
  );
};

export default App;
Example 2. Vanilla JS
index.js
styles.css
index.js
import { FeedSdk, InstantRemixing } from "@withkoji/vcc";
import './styles.css';

const instantRemixing = new InstantRemixing();
const feed = new FeedSdk();

var isRemixing = instantRemixing.isRemixing;
instantRemixing.onSetRemixing(isNowRemixing => {
    isRemixing = isNowRemixing;
    render();
});

var magazineOptions = instantRemixing.get(['settings', 'magazineOptions']);
var textOptions = instantRemixing.get(['settings', 'textOptions']);

// Set values on remix
instantRemixing.onValueChanged(([scope = "", key = ""], value) => {
    if (scope === 'settings' && key === 'magazineOptions') magazineOptions = value;
    if (scope === "settings" && key === "textOptions") textOptions = value;
    render();
});

// Optimize image urls using fastly params
const optimizeURL = url =>
  `${url}?fit=bounds&width=${window.innerWidth - 15}&height=${window.innerHeight - 15}&optimize=medium`;

const handleClick = e => {
    if (e.target.closest(".text")) {
        instantRemixing.onPresentControl(["settings", "textOptions"]);
    } else {
        instantRemixing.onPresentControl(['settings', 'magazineOptions']);
    }
};

// Handle dynamic resizing
var size = {};
const setSize = () => {
    let magazine = document.getElementById('magazine');
    if (magazine) {
        let oldSize = size;
        size = {
            height: magazine.height,
            width: magazine.width,
            top: magazine.offsetTop,
            left: magazine.offsetLeft
        };
        if (oldSize.height !== size.height || oldSize.width !== size.width || oldSize.top !== size.top || oldSize.left !== size.left) {
            render();
        }
    }
}
window.addEventListener("resize", setSize);

instantRemixing.ready();
feed.load();

// render app
const render = () => {
    let coverStyles = `
        width: ${size.width}px;
        height: ${size.height}px;
        top: ${size.top}px;
        left: ${size.left}px;
        background-image: url(${optimizeURL(magazineOptions.coverImage)});
    `;
    let titleStyles = `
        font-size: ${size.height ? (textOptions.fontSize / 200) * size.height : 20}px;
        color: ${textOptions.color};
        top: ${textOptions.position.y}%;
        left: ${textOptions.position.x}%;
    `;
    document.body.innerHTML = `
        <div id="wrapper">
            <img id="magazine" src="${magazineOptions.magazineName}"/>
            <div id="magazineCover" style="${coverStyles}">
                <div id="title" class="${isRemixing ? 'editable ': ''}text" style="${titleStyles}">
                    ${textOptions.title}
                </div>
            </div>
        </div>
    `;
    document.getElementById('wrapper').addEventListener('click', handleClick);
    document.getElementById('magazine').onload = setSize;
};

// render
render();
styles.css
/**
 * styles.css
 *
 * What it Does:
 *   This is your global CSS file, this file will style every page and
 *   component in your application.
 *
 * Things to Change:
 *   If you want any CSS properties to persist through all of your app
 *   or if any element you import has to be styled globally, this is the
 *   file to use.
 */

* {
    margin: 0;
    padding: 0;
}

h1 {
    margin: 16px;
    color: white;
}

body {
  background-color: black;
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

img {
    max-width: 100%;
}
div#wrapper {
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

div#magazineCover {
  background: no-repeat center center / cover;
  position: absolute;
  z-index: 0;
}

img#magazine {
  max-height: 100%;
  max-width: 100%;
  z-index: 1;
  pointer-events: none;
}

div#title {
  position: absolute;
  font-family: "Roboto", sans-serif;
  max-width: 35%;
  cursor: default;
}

.editable {
  border: 2px dashed grey;
}