Repository: pixelnest/presskit.html Branch: master Commit: 2be65759e4c7 Files: 59 Total size: 313.7 KB Directory structure: gitextract_oj4ovsy9/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets/ │ ├── _includes/ │ │ ├── about.html │ │ ├── appstore.html │ │ ├── assets.html │ │ ├── description.html │ │ ├── factsheet.html │ │ ├── footer.html │ │ ├── head.html │ │ ├── mailchimp.html │ │ ├── nav.html │ │ ├── playstore.html │ │ ├── recognition.html │ │ ├── scripts.html │ │ ├── team.html │ │ └── widgets.html │ ├── company.html │ ├── css/ │ │ ├── dark.css │ │ ├── light.css │ │ ├── normalize.css │ │ └── print.css │ ├── js/ │ │ └── hamburger.js │ ├── product.html │ ├── redirect.html │ └── templates/ │ ├── company.xml │ └── product.xml ├── bin/ │ ├── presskit │ ├── presskit-build.js │ └── presskit-new.js ├── data/ │ ├── data.xml │ └── product/ │ └── data.xml ├── docs/ │ ├── company/ │ │ └── index.html │ ├── css/ │ │ └── docco.css │ ├── example/ │ │ ├── css/ │ │ │ ├── master.css │ │ │ └── normalize.css │ │ ├── index.html │ │ ├── js/ │ │ │ └── hamburger.js │ │ └── product/ │ │ └── index.html │ └── product/ │ └── index.html ├── documentation ├── lib/ │ ├── assets.js │ ├── config.js │ ├── core/ │ │ ├── builder.js │ │ ├── generator.js │ │ ├── generator.test.js │ │ ├── loader.js │ │ ├── loader.test.js │ │ ├── parser.js │ │ ├── parser.test.js │ │ └── template.js │ ├── helpers/ │ │ ├── color-console.js │ │ ├── sfs.js │ │ ├── sfs.test.js │ │ ├── watcher.js │ │ └── zip.js │ └── index.js └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Numerous always-ignore extensions *.diff *.err *.orig *.log *.rej *.swo *.swp *.vi *~ *.sass-cache npm-debug.log # OS or Editor folders .DS_Store .cache .project .settings .tmproj nbproject Thumbs.db # NPM packages folder. node_modules/ # Brunch folder for temporary files. tmp/ # Brunch output folder. public/ build/ # Bower stuff. bower_components/ ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 20 notifications: slack: secure: qTKyJRT+HA7Bz0HsClvSBeVrN3M8w4Z6uXDNEkNP/o+1eBmDTl1WXV94Pvi/UkwuABm7XOvdLljvUcHKqalVL1PyiPYbl0Oe8ZPLgNtHKliQ0GY6qQdB4slLYJMzrSAyufID+3g3NW2Flxivb9dXT8lChYEuwXHm0rQlULdLCy+W9/qXPne1NGJN3TbZeGWqlhHI8+7ViQUUDCSPEJ+MZIDbKo6+FAkagJu2ZTsnMR21Ry+ZltaMSrImN2YkXbSEXN9mCKZVsQlDCFD4TnK/LVeMeBeo9MCTsJ7DjkmfqUsh1A7e9dyHdPLIzPPk+lln5PPRuAwjT0L/aPQ50XXDumFRZEAG/G6GyYqq1xRM6flizCjFzoF7vS9qfCapol0oz0jQ1J3AK6rlayXht/IAVAdEanbIDqWhIQQFt9LtE3lKx4yT7QTotmMOdlmkfs44gBa3mpF6d5+sqVSe6bh8rQAQnwoKrJqhDVw3fW1EnaFZzUsfZtmU8BnUgB+cAGMQ9hNeg9rwXq/UWZUejCENwPVQBV2slweT/RXqHEtBN2eUQmdTAx+TBFWLMON8992GqMDM9DnOnRnpvR5lw60/sd+8QBwCLQyjvo5ViVMUngck4bMg2gsOwgXd9UhsJHGvl9G2YOKhFyCR3284Qg8iwe/ZpFIUV9VBBn8Tjip/Wio= on_success: change on_failure: always ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Pixelnest Studio (pixelnest.io) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ![presskit.html](header.png) > Re-implementation of presskit() as a static site generator ![Build Status](https://travis-ci.org/pixelnest/presskit.html.svg?branch=master) [![NPM - Presskit](https://badgen.net/npm/v/presskit)](https://www.npmjs.com/package/presskit) [![Code Style - Standard](https://badgen.net/badge/code%20style/standard/f2a)](http://standardjs.com/) * [Why presskit.html?](#why) * [Showcase](#showcase) * [Roadmap](#roadmap) * [Quickstart](#quickstart-for-existing-presskit-users) * [Installation](#installation) * [Usage](#usage) * [Documentation](#documentation) * [Migration Guide](#migration-guide) * [Credits](#credits) _Created by [Pixelnest Studio](http://pixelnest.io/)._ This is a complete re-implementation, with a permissive MIT license, of [presskit()][dopresskit], which was originally created by [Rami Ismail](https://twitter.com/tha_rami) of [Vlambeer](http://www.vlambeer.com). Warning: **presskit.html** is, _currently_, a tool for developers. You need to know how to use the command-line — but that's all, to be honest. However, we plan to create a small app to simplify this process in the future. Stay tuned by following our [Twitter account](https://twitter.com/pixelnest). --- **presskit.html** is a tool to create a presskit for your company, products or games. To quote the original [presskit()][dopresskit]: > Developers & press both have the same goal: to bring great games to as many people as possible - after all, a good game is worth nothing if no-one plays it. For the press, finding out about a game but not having access to information & media for the game means that they can't write about it. Of course, developers want to spend their valuable time making games instead of press pages. > **presskit()** (pronounced _'do presskit'_) is the solution. Free for everyone, open and easy-to-use for both developers & press. Developers only have to spend an hour or so creating well-laid out press pages with everything the press needs to write to their hearts desire. Everybody wins. It uses an **almost-identical format and output** as its precursor. The goal is to be compatible, as much as possible. And even if presskit() was conceived with videogames in mind, we think that you can use it for any kind of product. Examples (built with **presskit.html**): * [Pixelnest Studio](http://pixelnest.io/presskit/) * [Fake Pizza Burger Studio](http://pixelnest.io/presskit.html/example/) The goal of **presskit.html** is to generate only static HTML pages — no PHP required at all. Just fill some XML data files, add some images, execute a command, and boom. It's done. **You already have a presskit and you want to use this tool instead of the old un-maintained PHP-based presskit()? [Read the migration guide](#migration-guide).** ## Why **Why reimplement [presskit()][dopresskit]?** **presskit.html** is basically a static site generator for presskit(). Everything is built once on your computer, and then distributed as static files to your users. We love the concept behind static site generators like [Jekyll](https://jekyllrb.com) or [Hugo](https://gohugo.io). These tools create lightweight static HTML pages, which are, by design, more secure and efficient than using a PHP server, for example. Moreover, if you use one of these tools to build your company or product's website, you can simply drop the result of **presskit.html** into your site directly, whatever the technology you are using — it's just HTML pages, after all. 😉 That's mainly why we built **presskit.html** — that's how we make our websites, and we can integrate our presskits more easily this way. We have also added some nice little things (like thumbnails generation, a "Press Copy Request" button, widgets integration, relations between products or an optional hamburger menu, for example) and created a more robust implementation of presskit() (which is, unfortunately, un-maintained since 2014). However, _we have tried to be as close as possible to the original presskit format and style._ In fact, comparing the output of **presskit.html** with the one of presskit() should be almost indistinguishable. This is by design: the aim of the original presskit() was to create an instantly-recognizable website — almost a standard in the videogame industry. You already have a presskit? Just try it: [follow our migration guide](#migration-guide), run **presskit.html** in the folder containing your presskit()-based `data.xml` and `images/` and you will have a ready to deploy set of HTML pages which are almost identical to what you already have. ## Showcase Built with **presskit.html**: * [Pixelnest Studio](http://pixelnest.io/presskit/) ([source code](https://github.com/pixelnest/presskit/)) * [Genix Lab](http://www.genix-lab.com/presskit/) * [Creative Brothers](http://www.creativebrothers.io/presskit/en/) _You are using **presskit.html**? Tell us or submit a pull request!_ Want to compare with presskit() websites? Check these ones: * [Vlambeer](http://www.vlambeer.com/press/) * [Flying Oak Games](http://www.flying-oak.com/presskit/index.php) ## Roadmap The roadmap is available on [Trello](https://trello.com/b/5T6BIyi3/open-source-presskit-html). ## Quickstart for existing presskit users 1. Install [Node.js](https://nodejs.org). 2. Open your terminal ("Terminal" on macOS, "cmd" on Windows). 3. Run `npm install -g presskit`. 4. Type `cd`, press space, and drag the folder containing your `data.xml` files. 5. Run `presskit build`. 6. Open the `build/` folder, double-click on index.html and… 🍾 ## Installation You will need a terminal and [Node.js](https://nodejs.org/). The simplest way to install **presskit.html** is to use [npm](http://npmjs.org/) (bundled with Node.js): ```shell npm install -g presskit ``` (Feeling fancy? Use [Yarn](https://yarnpkg.com/en/) instead.) This should add a globally available `presskit` command to your shell. To update to a new version of **presskit.html**, just type: ```shell npm update -g presskit ``` ## Usage Run this command: ``` presskit build ``` **presskit.html** will then scan your local working directory (where you are executing the command) and all direct sub-directories for `data.xml` files and `images/` folders. To launch your presskit with a server and automatically reload it each time your save a `data.xml`, just use: ``` presskit build --watch ``` You can also specify the folder to scan: ```shell presskit build path/to/folder ``` The `presskit` command does a bunch more (watch mode, generation of `data.xml`, etc.). Use `presskit -h` to learn more. In order to generate a complete presskit, you should have: - One `data.xml` for your company. - One `data.xml` per product in unique subfolders. - All assets (mostly images) located in an `images/` subfolder next to the corresponding `data.xml`. Example: ``` 📄 data.xml 📂 images/ 📄 header.png 📄 logo.png 📂 product-name-01/ 📄 data.xml 📂 images/ 📄 header.png 📄 logo.png 📄 screenshot1.png 📄 screenshot2.png ``` The `header.png` is used as the banner for the corresponding page. `logo.png` will be used as the product's brand. The arborescence above should generate a build folder containing: ``` 📂 build/ 📄 index.html 📂 images/ 📄 header.png 📄 logo.png 📄 images.zip 📄 logo.zip 📂 product-name-01/ 📄 index.html 📂 images/ 📄 header.png 📄 logo.png 📄 screenshot1.png 📄 screenshot2.png 📄 images.zip 📄 logo.zip 📂 css/ 📂 js/ ``` Simply copy **all** the files in the `build/` folder to your server… and you're done! _Note: the webserver is **not** included._ You can also [try our example](https://github.com/pixelnest/presskit.html/tree/master/data) from this repository, available online here: http://pixelnest.io/presskit.html/example/. ### Additional options of `presskit build` `presskit build` has a few other features. Use `presskit build -h` to discover them all. Two interesting ones are: - `presskit build --collapse-menu` uses a collapsed menu for the main navigation at the top (commonly-known as the "[hamburger button](https://en.wikipedia.org/wiki/Hamburger_button)") — only for small screens. This option toggles this behavior on all pages. - `presskit build --pretty-links` hides `index.html` at the end of URLs. You can combine all these options together, of course. ### Create `data.xml` files with `presskit new` You can also generate empty `data.xml` with the `presskit new` command. ## Documentation For a tag by tag walkthrough, open these links: - [Company `data.xml` file](http://pixelnest.io/presskit.html/company/) - [Product or game `data.xml` file](http://pixelnest.io/presskit.html/product/) **If you have never written a presskit before, those links are a must-read.** For a more detailed documentation about some specific features, see below. _NB: since **presskit.html** is 99% compatible with [presskit()][dopresskit], you can also just read the existing documentation there._ ### Tags **Warning: do not put XML tags inside your content.** For example, do not do this (note the `
`): ```xml Lorem ipsum
sit amet.
``` #### Widgets This is a new feature of **presskit.html**: you can put your widgets directly into your presskit pages. - Mailchimp `LIST_URL inside your signup form` - App Store `APP_ID` - Play Store `com.domain.yourappid` - Steam `STEAM_ID` - Humble Bundle `product_name/BUNDLE_ID` - Itch.io `ITCH_ID` - Game Jolt `PACKAGE_ID` - Bandcamp `BANDCAMP_ID` Just add the `` tag, and the widget providers that you want: ```xml //url.us3.list-manage.com/subscribe/post?u=USER_ID&id=LIST_ID 950812012 com.noodlecake.altosadventure 347160 steredenn/7SDLfk23hw 27992 8ReMi2Nw 1135613467 ``` We don't support other widgets for the moment, but feel free to send a pull request or submit an issue. **Warning: widgets import many scripts and assets. This may have a penalty on your page size and responsiveness.** #### Relations This is a new feature of **presskit.html**: you can specify relations between products using the `` tag. For example, on a product page, you could add something like: ```xml StarCraft […]        Expansion     StarCraft: Brood War           Sequel     StarCraft II    ``` _This tag should be added on the **main** page; not the related product._ At build time, a relation will be added to the product and its related product, with a link between the two. You can have as many relations as you want. You can use it to show DLCs, expansions, sequels, prequels, etc. **Warning**: you need to rebuild the presskit to see the changes. #### Other tags We recommend to read the [company](http://pixelnest.io/presskit.html/company/) and [product](http://pixelnest.io/presskit.html/product/) documentation pages for more information. New tags include `` and ``. More might be implemented later. ### Images For each `data.xml`, you can add an `images/` folder containing the assets of your product or game. - An image named `header.png` or `header.jpg` will be used for the page's banner. - An image named `logo.png` or `logo.jpg` will be used as your page's logo. - Each `jpg`, `jpeg`, `png` or `gif` will be displayed in the gallery. For each non-header/non-logo image, a thumbnail will be automatically generated during the build process. If you don't want to use the thumbnails, you can disable them with the `--ignore-thumbnails` option of `presskit build`. However, we do not recommend this: it might drastically increase the size of your pages. It can be a massive change: for example, on our presskit, one of our page has gone from 100mB to 4mB. We also convert gifs to small JPGs, that you can animate with a click. #### Logos You can provide multiple logos for a page. As long as they start with "logo", they will be displayed in the "Logo & Icon" section. This will work, for example: ``` 📄 logo01.png 📄 logo02.png 📄 logo03.jpg ``` #### Categories Inside the `images/` folder, you can sort images by categories. It's simple: put a few images into a subfolder (like `images/wallpapers/`), and a new category will be automatically added to the gallery. [You can find an example here.](http://pixelnest.io/presskit.html/example/product/#gallery-wallpapers) #### Favicon If a `favicon.ico` is found in the `images/` folder of a `data.xml`, it will be used as the favicon of this HTML page. It will not be exported in the `images.zip`, nor visible in the images gallery. ### Archives **presskit.html** will find every images and logos in the `images/` folder of a `data.xml`. Then, it will create two archives: `images.zip` and `logo.zip`. There's a small trick to know: if you provide one (or both) of these zips in your `images/` folder, **presskit.html** will just copy it directly, instead of overriding it. This is nice, because it allows you to provide a more complete (and heavy) zip. In this archive, you can, for example, put bigger gifs, images, artworks, or even videos. That's purely optional, and most products or games won't need a specially crafted archive. 😉 ## Migration Guide This tool is almost a drop-in replacement for presskit() (well, except for the fact that it generates HTML instead of using a PHP back-end — but that's simpler, not harder). Which mean that you can go in your folder containing the `data.xml` and `images/`, run `presskit build` and boom, you're done. _Well, almost._ We have made some breaking changes between this format and the original presskit() format. But be reassured: they are fairly small, and are, indeed, useful. Follow the guide. ### URLs This re-implementation of presskit() has a big difference: all your product URLs will break. With presskit(), you pointed to `/sheet.php?p=MYSUPERGAME` for the `MYSUPERGAME` page. Here, you will point to `/MYSUPERGAME/` directly (the `index.html` is not required, which makes prettier URLs). **This can't be changed.** We don't use PHP, but simple, robust and lightweight HTML files, and this difference is inevitable. ### External URLs presskit() didn't require the protocol (ie., `http` or `https`) for most URLs in the `data.xml`. For example: ```xml twitter.com/pixelnest/ twitter.com/pixelnest/ ``` Note that the `` has no `http` or `https` protocol before its destination. The problem with that is that we cannot deduce the protocol automatically. It will work seamlessly for the biggest sites like Facebook or Twitter, but we cannot guarantee that it will link correctly for everything. That's why we require that you specify the protocol for your URLs: ```xml twitter.com/pixelnest/ https://twitter.com/pixelnest/ ``` Otherwise, the URL will be relative to your presskit, and thus, will break. ### Company `data.xml` **This is recommended, but presskit.html is smart enough to detect the company `data.xml` automatically, if your file structure is correct.** Your main `data.xml` containing your company information should use a `` root tag for your XML document. Before: ```xml Pixelnest Studio ``` After: ```xml Pixelnest Studio ``` **Why?** It allows us to better differentiate the main `data.xml` from the others. And moreover, it does not make sense that the company `data.xml` is considered as a ``, right? ### Release dates The original presskit() assumed that you had only one release date for a product or game. And we all know that it's simply not true. That's why we handle multiple release dates. So, in your product/game `data.xml`, you must change your `` tag. Before: ```xml My Super Game 04 Feb, 2016 ``` After: ```xml My Super Game PC/Mac - 04 Feb, 2016 iOS/Android - 04 Feb, 2017 ``` **Why?** We all know that there's no single release date for a product or a game. ### Contacts In each `data.xml`, you can set a list of contacts: ```xml Inquiries contact@pizzaburger.studio ``` In presskit(), you needed to set these informations _only in the company page_. Each product then retrieved the values from the company and added them automatically. We modified that: now, you **need** to set these informations everywhere. That way, you can change mails and links for each product individually. --- ## Pixelnest Studio [Pixelnest Studio](http://pixelnest.io/) is a small indie company, creating games and apps. You can contact us on [Twitter at @pixelnest](https://twitter.com/pixelnest). Want to check our game, Steredenn? Go to http://steredenn.pixelnest.io/. ## Credits ### [presskit()][dopresskit] This couldn't have be made without the awesome work of [Rami Ismail](https://twitter.com/tha_rami) and the [presskit()][dopresskit] team. Thanks to them! ### Assets * The images used in this repository can be found on [Unsplash](https://unsplash.com/), a free provider of high-quality images. * Pizza gif is from [Giphy](http://giphy.com/). [dopresskit]: http://dopresskit.com ================================================ FILE: assets/_includes/about.html ================================================ {{#if company.title}}

About {{company.title}}

Boilerplate
{{company.description}}
More information
More information on {{company.title}}, our logo & relevant media are available here.
{{/if}} {{#each presskit.abouts}}

About {{title}}

Boilerplate
{{description}}
{{#if link}}
More information
More information on {{title}} is available here.
{{/if}}
{{/each}} ================================================ FILE: assets/_includes/appstore.html ================================================ ================================================ FILE: assets/_includes/assets.html ================================================ {{#if presskit.trailers}}

Videos

{{#each presskit.trailers}}
{{#youtube}}

{{../name}}YouTube {{#if ../download}}| Download{{/if}}

{{/youtube}} {{#vimeo}}

{{../name}}Vimeo {{#if ../download}}| Download{{/if}}

{{/vimeo}} {{#unless youtube}} {{#unless vimeo}} {{#download}}

{{../name}}Download

{{/download}} {{/unless}} {{/unless}}
{{/each}}
{{else}}

Videos

There are currently no trailers available for {{presskit.title}}. Check back later for more or contact us for specific requests!

{{/if}} {{#if hasScreenshots}}

Images

{{#each images.screenshotsWithCategory}}
{{/each}}
{{/if}} {{#if images.logos}}
{{#each images.logos}} {{/each}}
{{else}}

There are currently no logos or icons available for {{presskit.title}}. Check back later for more or contact us for specific requests!

{{/if}} ================================================ FILE: assets/_includes/description.html ================================================

Description

{{rawText presskit.description}}

History

{{#if presskit.history}}

{{rawText presskit.history}}

{{/if}} {{#each presskit.histories}}

{{header}}

{{rawText text}}

{{/each}} ================================================ FILE: assets/_includes/factsheet.html ================================================ ================================================ FILE: assets/_includes/footer.html ================================================ ================================================ FILE: assets/_includes/head.html ================================================ {{#if company.title}} {{presskit.title}} - {{company.title}} {{else}} {{presskit.title}} {{/if}} ================================================ FILE: assets/_includes/mailchimp.html ================================================ {{#if presskit.widgets.mailchimp}}

Mailing List

{{/if}} ================================================ FILE: assets/_includes/nav.html ================================================ ================================================ FILE: assets/_includes/playstore.html ================================================ ================================================ FILE: assets/_includes/recognition.html ================================================ {{#if presskit.awards}}

Awards & Recognition

    {{#each presskit.awards}}
  • "{{description}}" {{info}}
  • {{/each}}
{{/if}} {{#if presskit.quotes}}

Selected Articles

    {{#each presskit.quotes}}
  • {{description}}
    {{name}}, {{website}}
  • {{/each}}
{{/if}} {{#if presskit.additionals}}
{{#each presskit.additionals}}
{{title}}
{{description}} {{domainURL link}}.
{{/each}}
{{/if}} ================================================ FILE: assets/_includes/scripts.html ================================================ {{#if hamburger}} {{/if}} {{#if analytics}} {{/if}} ================================================ FILE: assets/_includes/team.html ================================================
{{#if presskit.credits}}
{{#if title}}

{{title}}

{{else}}

{{presskit.title}} Credits

{{/if}}
{{#each presskit.credits}}
{{person}}
{{#if website}} {{role}} {{else}} {{role}} {{/if}}
{{/each}}
{{/if}} {{#if presskit.contacts}} {{/if}}
================================================ FILE: assets/_includes/widgets.html ================================================ {{#if presskit.widgets}}

Widgets

{{#if presskit.widgets.appstore}}

{{>appstore}}

{{/if}} {{#if presskit.widgets.playstore}}

{{>playstore}}

{{/if}}
{{#if presskit.widgets.steam}}

{{/if}} {{#if presskit.widgets.humble}}

{{/if}} {{#if presskit.widgets.itch}}

{{/if}} {{#if presskit.widgets.gamejolt}}

{{/if}} {{#if presskit.widgets.bandcamp}}

{{/if}}
{{/if}} ================================================ FILE: assets/company.html ================================================ {{>head}}
{{>nav}}
{{>factsheet}}
{{>description}}

Projects

{{>assets}} {{>recognition}} {{>team title="Team & Repeating Collaborator"}}
{{>footer}}
{{>scripts}} ================================================ FILE: assets/css/dark.css ================================================ @charset "UTF-8"; /* ---------------------------------------------------------- *\ * Globals. \* ---------------------------------------------------------- */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } body { position: relative; min-height: 100vh; margin: 0; } /* ---------------------------------------------------------- *\ * Typography & colors. \* ---------------------------------------------------------- */ html { color: #cccccc; background: #1a1a2e; font: 62.5%/1.6 "Trebuchet MS", Arial, Helvetica, sans-serif; } body { font-size: 1.26em; } h1, h2 { color: #e0e0e0; font-family: Georgia, "Times New Roman", Times, serif; font-weight: normal; } ::-moz-selection { color: white; background: #4a90d9; } ::selection { color: white; background: #4a90d9; } /* ---------------------------------------------------------- *\ * Basic elements. \* ---------------------------------------------------------- */ h1 { margin: 0; font-size: 2.8em; } h2 { margin: 25px 0 15px 0; font-size: 1.9em; } h3 { font-size: inherit; font-weight: bold; } img { max-width: 100%; } p { margin: 15px 0; } p:last-child { margin-bottom: 0; } /* Lists. */ ul, ol { margin: 15px 0; padding-left: 30px; } ul:last-child, ol:last-child { margin-bottom: 0; } ul { list-style-type: square; } dl { margin: 0; } dt { color: #e0e0e0; font-weight: bold; } dd { margin: 0; } dd + dt { margin-top: 15px; } /* ---------------------------------------------------------- *\ * Layout. \* ---------------------------------------------------------- */ body { padding: 20px 0; } .page { max-width: 1180px; margin: 0 auto; padding: 0 25px; } @media (min-width: 800px) { .page { display: -webkit-box; display: -ms-flexbox; display: flex; } .page-nav { min-width: 25%; } .page-wrapper { max-width: 75%; } .factsheet { min-width: 33%; } } /* ---------------------------------------------------------- *\ * Micro Components. \* ---------------------------------------------------------- */ .clearfix::after { display: table; clear: both; content: " "; } .capitalize { text-transform: capitalize; } /* ---------------------------------------------------------- *\ * Components - Links. \* ---------------------------------------------------------- */ a, a:visited { color: #5dade2; text-decoration: none; } a:hover { color: #85c1e9; text-decoration: underline; } a:focus { outline: thin dotted; } a:active, a:hover { outline: none; } /* ---------------------------------------------------------- *\ * Components - Page Header. \* ---------------------------------------------------------- */ .page-header img { display: block; } /* ---------------------------------------------------------- *\ * Components - Nav. \* ---------------------------------------------------------- */ .nav { margin-bottom: 30px; } .nav__title { padding: 0 15px; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; line-height: 1; } .nav__title a, .nav__title a:visited, .nav__title a:hover { color: inherit; text-decoration: none; } .nav__subtitle { padding: 5px 15px; } .nav__list { padding: 0; list-style: none; } .nav__item, .nav__item:visited { display: block; padding: 5px 15px; color: inherit; } .nav__item:hover { color: inherit; background: rgba(255, 255, 255, 0.05); text-decoration: none; } .nav__item:focus { background: rgba(255, 255, 255, 0.05); outline: none; } /* ---------------------------------------------------------- *\ * Nav - Hamburger. \* ---------------------------------------------------------- */ .nav__toggle { display: none; margin: 15px 0; padding: 0 15px; } .nav__toggle .button { display: block; height: 30px; } /* Nav list variant for hamburger feature. */ .nav__list--slider { overflow-y: hidden; max-height: 0; -webkit-transition: max-height 0.5s ease-in-out; -moz-transition: max-height 0.5s ease-in-out; -o-transition: max-height 0.5s ease-in-out; transition: max-height 0.5s ease-in-out; } @media (min-width: 800px) { .nav__toggle { /* Always hide the button on big screens. */ display: none !important; } .nav__list--slider { /* Always show the list on big screens. So disable slider. */ max-height: unset !important; } } /* ---------------------------------------------------------- *\ * Components - Block. \* ---------------------------------------------------------- */ .block { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #333355; } .block__notice { margin-bottom: 15px; padding: 10px; color: #5dade2; background: #16213e; border: 1px solid rgba(93, 173, 226, 0.3); border-radius: 4px; } .block__notice a { color: inherit; } /* ---------------------------------------------------------- *\ * Components - Grid. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .grid { display: -webkit-box; display: -ms-flexbox; display: flex; } .grid__item:not(:first-child) { padding-left: 25px; } .grid__item--flexible { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; } } /* ---------------------------------------------------------- *\ * Components - Images Gallery. \* ---------------------------------------------------------- */ .gallery { margin-left: -25px; font-size: 0; /* Disable the small space below the images. */ } .gallery__item { float: left; width: 100%; padding-bottom: 25px; padding-left: 25px; } .gallery__item img { width: 100%; } .gallery__item a[href$=".gif"] { position: relative; display: block; } .gallery__item a[href$=".gif"]:focus:active { outline: none; } .gallery__item a[href$=".gif"]::after { position: absolute; z-index: 1; top: 0; left: 0; display: none; width: 100%; height: 100%; padding: 20px; background: rgba(0, 0, 0, 0.45); content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAABGdBTUEAALGPC/xhBQAAAFFQTFRFAAAA/////////////////////////////Pz8/Pz8/Pz8/f39/f39/f39/f39/f39/f39/Pz8/Pz8/Pz8/Pz8/v7+/f39/f39/f39/f39/f395zF2nQAAABt0Uk5TAAEJCxQeIEpXYGFnc3V2d5WarK2/xNLb3N3m8ZFI2gAAAOlJREFUOMuVlckOgzAMRAdIWUrKUgK4/v8P7QWVLA64c0RPY9mxByBQ3U/LSrQuU18jKzM49uQGI2KVJY5Etkq5ZmNBWxNhheWMbOFz5cxZzaXnd8Exz6en5UvZXx98o6OjarsDtyop/Hrnixt/zm3x/AggGQCj/6UFHpLpAMBFICRTB9Qcg6JpjV4ABdMekwSmphMWGUTRBaYL1gwYma6gLBiYEvYr8HTZ9aXzzVDYjHo88sA7SgaufkJhKRI7Zgdg0K5ZtLiC3bG46lPQH5f6XPUBoI8UdUjpY08fpH9EMwAzqsL+5vfxBcKnb1vURTbUAAAAAElFTkSuQmCC"); align-items: center; justify-content: center; } .gallery__item a[href$=".gif"].show-overlay::after { display: flex; } @media (min-width: 500px) { .gallery__item { width: 50%; } } /* ---------------------------------------------------------- *\ * Components - Logo. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .logo { max-width: 49%; } } /* ---------------------------------------------------------- *\ * Components - Video player. \* ---------------------------------------------------------- */ .video-player { position: relative; padding-bottom: 56.25%; /* 16:9 */ } .video-player__frame { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } /* ---------------------------------------------------------- *\ * Components - Quote. \* ---------------------------------------------------------- */ .quote__content { margin: 0; } .quote__content::before { content: open-quote; } .quote__content::after { content: close-quote; } .quote__author::before { content: "-"; } /* ---------------------------------------------------------- *\ * Components - Button. \* ---------------------------------------------------------- */ .button { display: inline-block; padding: 5px 10px; color: #cccccc; background: -webkit-linear-gradient(top, #2a2a4a, #1a1a2e); background: linear-gradient(to bottom, #2a2a4a, #1a1a2e); border: 1px solid #444466; border-radius: 5px; } .button:hover { cursor: pointer; text-decoration: none; } .button:focus { border: 1px solid #4a90d9; outline: none; } .button:active { color: white; background: -webkit-linear-gradient(top, #4a90d9, #357abd); background: linear-gradient(to bottom, #4a90d9, #357abd); } /* ---------------------------------------------------------- *\ * Widgets. \* ---------------------------------------------------------- */ .widget { width: 100%; } .widget--steam { height: 190px; } .widget--humble { height: 328px; } @media (max-width: 505px) { .widget--humble { height: 205px; } } .widget--itch { height: 167px; } .widget--gamejolt { height: 245px; } .widget--bandcamp { width: 100%; height: 120px; border: 0; } /* ---------------------------------------------------------- *\ * Hacks. \* ---------------------------------------------------------- */ @media (min-width: 800px) { ._team-fix-margin-top { margin-top: -15px; } } /* ---------------------------------------------------------- *\ * Components - Footer. \* ---------------------------------------------------------- */ .page-footer { color: #888888; } .page-footer a, .page-footer a:visited { color: #5dade2; } ================================================ FILE: assets/css/light.css ================================================ @charset "UTF-8"; /* ---------------------------------------------------------- *\ * Globals. \* ---------------------------------------------------------- */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } body { position: relative; min-height: 100vh; margin: 0; } /* ---------------------------------------------------------- *\ * Typography & colors. \* ---------------------------------------------------------- */ html { color: #444444; font: 62.5%/1.6 "Trebuchet MS", Arial, Helvetica, sans-serif; } body { font-size: 1.26em; } h1, h2 { font-family: Georgia, "Times New Roman", Times, serif; font-weight: normal; } ::-moz-selection { color: white; background: #3398ff; } ::selection { color: white; background: #3398ff; } /* ---------------------------------------------------------- *\ * Basic elements. \* ---------------------------------------------------------- */ h1 { margin: 0; font-size: 2.8em; } h2 { margin: 25px 0 15px 0; font-size: 1.9em; } h3 { font-size: inherit; font-weight: bold; } img { max-width: 100%; } p { margin: 15px 0; } p:last-child { margin-bottom: 0; } /* Lists. */ ul, ol { margin: 15px 0; padding-left: 30px; } ul:last-child, ol:last-child { margin-bottom: 0; } ul { list-style-type: square; } dl { margin: 0; } dt { font-weight: bold; } dd { margin: 0; } dd + dt { margin-top: 15px; } /* ---------------------------------------------------------- *\ * Layout. \* ---------------------------------------------------------- */ body { padding: 20px 0; } .page { max-width: 1180px; margin: 0 auto; padding: 0 25px; } @media (min-width: 800px) { .page { display: -webkit-box; display: -ms-flexbox; display: flex; } .page-nav { min-width: 25%; } .page-wrapper { max-width: 75%; } .factsheet { min-width: 33%; } } /* ---------------------------------------------------------- *\ * Micro Components. \* ---------------------------------------------------------- */ .clearfix::after { display: table; clear: both; content: " "; } .capitalize { text-transform: capitalize; } /* ---------------------------------------------------------- *\ * Components - Links. \* ---------------------------------------------------------- */ a, a:visited { color: #0077dd; text-decoration: none; } a:hover { color: #005599; text-decoration: underline; } a:focus { outline: thin dotted; } a:active, a:hover { outline: none; } /* ---------------------------------------------------------- *\ * Components - Page Header. \* ---------------------------------------------------------- */ .page-header img { display: block; } /* ---------------------------------------------------------- *\ * Components - Nav. \* ---------------------------------------------------------- */ .nav { margin-bottom: 30px; } .nav__title { padding: 0 15px; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; line-height: 1; } .nav__title a, .nav__title a:visited, .nav__title a:hover { color: inherit; text-decoration: none; } .nav__subtitle { padding: 5px 15px; } .nav__list { padding: 0; list-style: none; } .nav__item, .nav__item:visited { display: block; padding: 5px 15px; color: inherit; } .nav__item:hover { color: inherit; background: rgba(0, 0, 0, 0.05); text-decoration: none; } .nav__item:focus { background: rgba(0, 0, 0, 0.05); outline: none; } /* ---------------------------------------------------------- *\ * Nav - Hamburger. \* ---------------------------------------------------------- */ .nav__toggle { display: none; margin: 15px 0; padding: 0 15px; } .nav__toggle .button { display: block; height: 30px; } /* Nav list variant for hamburger feature. */ .nav__list--slider { overflow-y: hidden; max-height: 0; -webkit-transition: max-height 0.5s ease-in-out; -moz-transition: max-height 0.5s ease-in-out; -o-transition: max-height 0.5s ease-in-out; transition: max-height 0.5s ease-in-out; } @media (min-width: 800px) { .nav__toggle { /* Always hide the button on big screens. */ display: none !important; } .nav__list--slider { /* Always show the list on big screens. So disable slider. */ max-height: unset !important; } } /* ---------------------------------------------------------- *\ * Components - Block. \* ---------------------------------------------------------- */ .block { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid rgb(221, 221, 221); } .block__notice { margin-bottom: 15px; padding: 10px; color: #027194; background: #ebf7fd; border: 1px solid rgba(45,112,145,0.3); border-radius: 4px; } .block__notice a { color: inherit; } /* ---------------------------------------------------------- *\ * Components - Grid. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .grid { display: -webkit-box; display: -ms-flexbox; display: flex; } .grid__item:not(:first-child) { padding-left: 25px; } .grid__item--flexible { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; } } /* ---------------------------------------------------------- *\ * Components - Images Gallery. \* ---------------------------------------------------------- */ .gallery { margin-left: -25px; font-size: 0; /* Disable the small space below the images. */ } .gallery__item { float: left; width: 100%; padding-bottom: 25px; padding-left: 25px; } .gallery__item img { width: 100%; } .gallery__item a[href$=".gif"] { position: relative; display: block; } .gallery__item a[href$=".gif"]:focus:active { outline: none; } .gallery__item a[href$=".gif"]::after { position: absolute; z-index: 1; top: 0; left: 0; display: none; width: 100%; height: 100%; padding: 20px; background: rgba(0, 0, 0, 0.25); content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAABGdBTUEAALGPC/xhBQAAAFFQTFRFAAAA/////////////////////////////Pz8/Pz8/Pz8/f39/f39/f39/f39/f39/f39/Pz8/Pz8/Pz8/Pz8/v7+/f39/f39/f39/f39/f395zF2nQAAABt0Uk5TAAEJCxQeIEpXYGFnc3V2d5WarK2/xNLb3N3m8ZFI2gAAAOlJREFUOMuVlckOgzAMRAdIWUrKUgK4/v8P7QWVLA64c0RPY9mxByBQ3U/LSrQuU18jKzM49uQGI2KVJY5Etkq5ZmNBWxNhheWMbOFz5cxZzaXnd8Exz6en5UvZXx98o6OjarsDtyop/Hrnixt/zm3x/AggGQCj/6UFHpLpAMBFICRTB9Qcg6JpjV4ABdMekwSmphMWGUTRBaYL1gwYma6gLBiYEvYr8HTZ9aXzzVDYjHo88sA7SgaufkJhKRI7Zgdg0K5ZtLiC3bG46lPQH5f6XPUBoI8UdUjpY08fpH9EMwAzqsL+5vfxBcKnb1vURTbUAAAAAElFTkSuQmCC"); align-items: center; justify-content: center; } .gallery__item a[href$=".gif"].show-overlay::after { display: flex; } @media (min-width: 500px) { .gallery__item { width: 50%; } } /* ---------------------------------------------------------- *\ * Components - Logo. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .logo { max-width: 49%; } } /* ---------------------------------------------------------- *\ * Components - Video player. \* ---------------------------------------------------------- */ .video-player { position: relative; padding-bottom: 56.25%; /* 16:9 */ } .video-player__frame { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } /* ---------------------------------------------------------- *\ * Components - Quote. \* ---------------------------------------------------------- */ .quote__content { margin: 0; } .quote__content::before { content: open-quote; } .quote__content::after { content: close-quote; } .quote__author::before { content: "-"; } /* ---------------------------------------------------------- *\ * Components - Button. \* ---------------------------------------------------------- */ .button { display: inline-block; padding: 5px 10px; background: -webkit-linear-gradient(top, white, #fafafa); background: linear-gradient(to bottom, white, #fafafa); border: 1px solid #cccccc; border-radius: 5px; } .button:hover { cursor: pointer; text-decoration: none; } .button:focus { border: 1px solid #3398ff; outline: none; } .button:active { color: white; background: -webkit-linear-gradient(top, #3398ff, #0077dd); background: linear-gradient(to bottom, #3398ff, #0077dd); } /* ---------------------------------------------------------- *\ * Widgets. \* ---------------------------------------------------------- */ .widget { width: 100%; } .widget--steam { height: 190px; } .widget--humble { height: 328px; } @media (max-width: 505px) { .widget--humble { height: 205px; } } .widget--itch { height: 167px; } .widget--gamejolt { height: 245px; } .widget--bandcamp { width: 100%; height: 120px; border: 0; } /* ---------------------------------------------------------- *\ * Hacks. \* ---------------------------------------------------------- */ @media (min-width: 800px) { ._team-fix-margin-top { margin-top: -15px; } } ================================================ FILE: assets/css/normalize.css ================================================ /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ /** * 1. Change the default font family in all browsers (opinionated). * 2. Correct the line height in all browsers. * 3. Prevent adjustments of font size after orientation changes in * IE on Windows Phone and in iOS. */ /* Document ========================================================================== */ html { font-family: sans-serif; /* 1 */ line-height: 1.15; /* 2 */ -ms-text-size-adjust: 100%; /* 3 */ -webkit-text-size-adjust: 100%; /* 3 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers (opinionated). */ body { margin: 0; } /** * Add the correct display in IE 9-. */ article, aside, footer, header, nav, section { display: block; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * Add the correct display in IE 9-. * 1. Add the correct display in IE. */ figcaption, figure, main { /* 1 */ display: block; } /** * Add the correct margin in IE 8. */ figure { margin: 1em 40px; } /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * 1. Remove the gray background on active links in IE 10. * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. */ a { background-color: transparent; /* 1 */ -webkit-text-decoration-skip: objects; /* 2 */ } /** * Remove the outline on focused links when they are also active or hovered * in all browsers (opinionated). */ a:active, a:hover { outline-width: 0; } /** * 1. Remove the bottom border in Firefox 39-. * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { border-bottom: none; /* 1 */ text-decoration: underline; /* 2 */ text-decoration: underline dotted; /* 2 */ } /** * Prevent the duplicate application of `bolder` by the next rule in Safari 6. */ b, strong { font-weight: inherit; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font style in Android 4.3-. */ dfn { font-style: italic; } /** * Add the correct background and color in IE 9-. */ mark { background-color: #ff0; color: #000; } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in * all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Embedded content ========================================================================== */ /** * Add the correct display in IE 9-. */ audio, video { display: inline-block; } /** * Add the correct display in iOS 4-7. */ audio:not([controls]) { display: none; height: 0; } /** * Remove the border on images inside links in IE 10-. */ img { border-style: none; } /** * Hide the overflow in IE. */ svg:not(:root) { overflow: hidden; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers (opinionated). * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: sans-serif; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Show the overflow in IE. * 1. Show the overflow in Edge. */ button, input { /* 1 */ overflow: visible; } /** * Remove the inheritance of text transform in Edge, Firefox, and IE. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` * controls in Android 4. * 2. Correct the inability to style clickable types in iOS and Safari. */ button, html [type="button"], /* 1 */ [type="reset"], [type="submit"] { -webkit-appearance: button; /* 2 */ } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } /** * Change the border, margin, and padding in all browsers (opinionated). */ fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } /** * 1. Correct the text wrapping in Edge and IE. * 2. Correct the color inheritance from `fieldset` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out * `fieldset` elements in all browsers. */ legend { box-sizing: border-box; /* 1 */ color: inherit; /* 2 */ display: table; /* 1 */ max-width: 100%; /* 1 */ padding: 0; /* 3 */ white-space: normal; /* 1 */ } /** * 1. Add the correct display in IE 9-. * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. */ progress { display: inline-block; /* 1 */ vertical-align: baseline; /* 2 */ } /** * Remove the default vertical scrollbar in IE. */ textarea { overflow: auto; } /** * 1. Add the correct box sizing in IE 10-. * 2. Remove the padding in IE 10-. */ [type="checkbox"], [type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Correct the cursor style of increment and decrement buttons in Chrome. */ [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. */ [type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in IE 9-. * 1. Add the correct display in Edge, IE, and Firefox. */ details, /* 1 */ menu { display: block; } /* * Add the correct display in all browsers. */ summary { display: list-item; } /* Scripting ========================================================================== */ /** * Add the correct display in IE 9-. */ canvas { display: inline-block; } /** * Add the correct display in IE. */ template { display: none; } /* Hidden ========================================================================== */ /** * Add the correct display in IE 10-. */ [hidden] { display: none; } ================================================ FILE: assets/css/print.css ================================================ @charset "UTF-8"; /* ---------------------------------------------------------- *\ * Print stylesheet for presskit.html \* ---------------------------------------------------------- */ @media print { /* Reset layout to single column. */ body { padding: 0; font-size: 12pt; } .page { display: block; max-width: none; padding: 0; } .page-wrapper { max-width: none; } /* Hide navigation. */ .page-nav { display: none; } /* Full-width factsheet + description. */ .grid { display: block; } .grid__item:not(:first-child) { padding-left: 0; } /* Factsheet as a horizontal block before content. */ .factsheet { min-width: 100%; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #ccc; } .factsheet__list { columns: 2; column-gap: 30px; } .factsheet__list dt { break-after: avoid; } .factsheet__list dd + dt { break-before: auto; } /* Images: keep them reasonable. */ .page-header img { max-height: 200px; object-fit: contain; } .gallery__item { width: 33.33%; page-break-inside: avoid; } /* Hide interactive elements. */ .nav__toggle, .video-player, .widget, .press { display: none; } /* Links: show URL after link text. */ a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; font-style: italic; } /* But not for internal nav or anchor links. */ a[href^="#"]::after, a[href^="."]::after, a[href^="mailto"]::after { content: none; } /* Avoid page breaks inside blocks. */ .block { page-break-inside: avoid; } /* Footer. */ .page-footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #ccc; font-size: 0.85em; } } ================================================ FILE: assets/js/hamburger.js ================================================ 'use strict' ;(function () { // Preamble: this code is very specific, and clearly made for then // main nav. // Currently, this functionality is not need elsewhere, so we didn't // bother to generalized this. // No guards too. // To disable this feature, just remove the script tag. window.addEventListener('DOMContentLoaded', function () { const container = document.querySelector('#hamburger') const button = document.querySelector('#hamburger-toggle') const target = document.querySelector('#hamburger-target') // Show hamburger (hidden by default if no js or hamburger disabled). container.style.display = 'block' // Get target height. const baseHeight = getElementHeight(target) // Do that after the height calculation! target.className += ' nav__list--slider' button.addEventListener('click', function (e) { e.preventDefault() e.stopPropagation() const currentMaxHeight = parseInt(target.style.maxHeight, 10) // If not set or 0, toggle to full height. // Otherwise, hide. if (!currentMaxHeight || currentMaxHeight === 0) { target.style.cssText = 'max-height: ' + baseHeight + 'px' } else { target.style.cssText = 'max-height: 0px' } }) }) // Clone an element outside the screen to safely get its height, then destroy it. function getElementHeight (element) { const clone = element.cloneNode(true) clone.style.cssText = 'visibility: hidden; display: block; margin: -999px 0' const height = (element.parentNode.appendChild(clone)).clientHeight element.parentNode.removeChild(clone) return height } })() ================================================ FILE: assets/product.html ================================================ {{>head}}
{{>nav}}
{{>factsheet}}
{{>description}}

Features

    {{#each presskit.features}}
  • {{.}}
  • {{/each}}
{{>mailchimp}} {{>assets}} {{>widgets}} {{>recognition}} {{>about}} {{>team}}
{{>footer}}
{{>scripts}} ================================================ FILE: assets/redirect.html ================================================ Redirecting…

Click here to go to {{name}}.

================================================ FILE: assets/templates/company.xml ================================================
================================================ FILE: assets/templates/product.xml ================================================ ================================================ FILE: bin/presskit ================================================ #!/usr/bin/env node const chalk = require('chalk') const {program} = require('commander') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- const version = require('../package.json').version const description = `Re-implementation of ${chalk.blue('http://dopresskit.com/')} as a static site generator. Read the documentation on ${chalk.blue('https://github.com/pixelnest/presskit.html')}.` // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- program .version(version) .description(description) .command('build [entrypoint]', 'build the presskit from the folder [entry point]') .command('new [destination]', 'create an empty template in the [destination] folder') .parse(process.argv) ================================================ FILE: bin/presskit-build.js ================================================ 'use strict' const path = require('upath') const chalk = require('chalk') const { program } = require('commander') const presskit = require('../lib/index') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- const version = require('../package.json').version const usage = chalk.green('[options]') + ' ' + chalk.yellow('') const description = `Generate a presskit based on information found in \`data.xml\` files. The format and the ouput are (nearly) the same as ${chalk.blue('http://dopresskit.com/')}. However, this command will generate static HTML files. More information on ${chalk.blue('https://github.com/pixelnest/presskit.html#usage')}.` // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- program .version(version) .description(description) .usage(usage) .option( '-o, --output [destination]', 'output the build folder to the [destination] (defaults to ./build)', path.join(process.cwd(), 'build') ) .option('-w, --watch', 'watch project for changes and re-generate if needed') .option('-d, --dev', 'add monitoring of CSS and templates in watch mode') .option('-p, --port [8080]', 'set the server port to [8080]', 8080) .option('-D, --clean-build-folder', 'delete the build folder beforehand') .option('-L, --pretty-links', 'hide index.html at the end of links') .option('-M, --collapse-menu', 'use a collapsed menu (hamburger) on small screens') .option('-B, --base-url [base]', 'prefix absolute urls with [base] (if your presskit is not at the root of your server)', '/') .option('-T, --ignore-thumbnails', 'use original images in galleries instead of thumbnails (will increase pages size)') .option('-C, --css [name]', 'CSS theme to use: a built-in name (light, dark) or a path to a custom file', 'light') .parse(process.argv) const opts = program.opts() presskit.runBuildCommand({ entryPoint: program.args[0], cleanBuildFolder: opts.cleanBuildFolder, ignoreThumbnails: opts.ignoreThumbnails, prettyLinks: opts.prettyLinks, baseUrl: opts.baseUrl, css: opts.css, hamburger: opts.collapseMenu, output: opts.output, watch: opts.watch, port: opts.port, dev: opts.dev }) ================================================ FILE: bin/presskit-new.js ================================================ 'use strict' const chalk = require('chalk') const { program } = require('commander') const presskit = require('../lib/index') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- const version = require('../package.json').version const usage = chalk.green('[options]') + ' ' + chalk.yellow('') const description = `Create an empty \`data.xml\` file and its \`images/\` folder in the folder (current working directory by default). There are two template types available: ${chalk.blue('company')} (default) or ${chalk.blue('product')}.` // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- program .version(version) .description(description) .usage(usage) .option('-t, --type [company]', 'set the type of the new `data.xml` file', 'company') .parse(process.argv) presskit.runNewCommand(program.opts().type, program.args[0] || process.cwd()) ================================================ FILE: data/data.xml ================================================ Pizza Burger Studio Paris, France February 6, 2014 https://github.com/pixelnest/presskit.html +42 (3) 42 42 42 42 42
42 street of Pixelnest 35000 Rennes France
Twitter @pizzaburgerstudio https://twitter.com/pizzaburgerstudio Facebook Page https://facebook.com/pizzaburgerstudio Skype callto:pizzaburgerstudio contact@pizzaburger.studio UA-42424242-0 This is a fake presskit for a fake company called Pizza Burger Studio.
Built with presskit.html
This presskit is generated by presskit.html, a tool created by Pixelnest Studio.
Free and Open Source
Find presskit.html on https://github.com/pixelnest/presskit.html
Example from YouTube. Don't give a full link: the ID is enough er416Ad3R1g Example from both YouTube and Vimeo YH3c1QZzRK4 108650530 Interview with Damien Mayance and Matthieu Oger of Pixenest Studio https://github.com/pixelnest/presskit.html An award Name, location, 29 April, 1988 A nomination Name, location, 03 October, 1988 There's not enough pizza in your life. It's never enough. @mrhelmut Tweet https://twitter.com/mrhelmut/status/717276362814447616 Patricia Pizza Twitter @patpiz at https://twitter.com/patpiz Bob Burger Twitter @bobburg at https://twitter.com/bobburg Patricia Pizza Founder, Developer, Pizza Burger Studio Bob Burger Founder, Designer, Pizza Burger Studio Jake Burgza Artist, Collaborator http://www.jakeburgza.com Sophia Pizer Musician, Collaborator http://www.sophiapizer.com Inquiries contact@pizzaburger.studio Twitter https://twitter.com/pizzaburgerstudio Facebook https://facebook.com/pizzaburgerstudio Web http://pizzaburger.studio
================================================ FILE: data/product/data.xml ================================================ My Super Game http://pizzaburger.studio/mysupergame http://pizzaburger.studio/mysupergame/request/ Publisher Pixelnest Studio https://pixelnest.io/ Rennes, France Distributor Pizza Oven LLC 04 Feb, 2016 10 Oct, 2016 PC / Mac http://itch.io/ Steam http://steampowered.com/ Inc New Store (TBA) EUR 20 USD 20 GBP 16 JPY 2300        DLC     My Super Game: Ultimate Edition    Here goes a quick description of your product or game. Be concise and explain in very few words the concept or the gameplay and why it's really cool and why everyone should use or play it. Add some storytelling here. Not the scenario of your game, but rather some backgrounds behind the creation process: why are you making this game? Most projects starts with a cool story. List some "Key Sellings Points" to grab player's attention. Don't be too generic ("pixel art graphics!"), don't be too pretentious ("most incredible game experience!"). Also, people like numbers, so you can add some (450 weapons, hundreds of levels, dozens of hours of playtime!). Need ideas? Maybe explain some game modes? It would be a nice place to say something about multiplayer, if you have some. Have you translated your game? (You probably should btw.) Release Trailer EtXajayBLzw Gameplay Video #2 EPNK1j3TMjU Early Access Trailer EtyQMcc19xY Short gameplay preview: Burger vs Pizza https://github.com/pixelnest/presskit.html 950812012 com.noodlecake.altosadventure 347160 steredenn/7SDLfk23hw 27992 1135613467 Game of the year without a doubt Saint-Père-Marc-En-Poulet (France), 04 February, 2016 Best soundtrack A great game festival (World), 01 October, 2015 Best MYGAMENGINE game Deep into the woods (Forest), 31 March, 2014 This is my favorite game of all time. Mum At home http://at.home/ A very serious quote you're very proud of by someone you respect. Master Master's website http://mast.er/ 10/10 would play it again and again. A friendly anonymous Steam reviewer Steam review http://steam.review/ Original Soundtrack (OST) Composed by an awesome musician. Listen for free, download for $3 at http://zandernoriega.bandcamp.com/album/steredenn-original-soundtrack Release announcement Announcement are exciting, so we usually make blog posts or news about it on http://pixelnest.io/journal/ Pixelnest Studio Pixelnest Studio is a small French studio which creates games, websites and apps. They made Steredenn, a roguelike-shmup in big pixels. They are also behind presskit.html, which you are probably using if you are reading this. https://pixelnest.io/ Pizza Oven LLC This is a fake company to illustrate the fact that you can have multiple about tags in your product page. Krokmou Bot Leader, Game Designer, Pixelnest Studio Hiccup Developer, Pixelnest Studio Astrid http://www.astridsupergame.com Musician, Freelancer Inquiries mysupergame@pizzaburger.studio Twitter https://twitter.com/pizzaburgerstudio Facebook https://facebook.com/pizzaburgerstudio Web http://mysupergame.pizzaburger.studio/ ================================================ FILE: docs/company/index.html ================================================ Company — presskit.html
  • Company — presskit.html

  • This is your company page. It should not focus on a specific product, but present your whole company. The journalists won’t, most of the time, know you or your studio. With this page, you will have an opportunity to show your people, your work, your history and your contributions in a standard format. This is not a replacement for your website, but a quick way for the press to know you faster.

    Check the actual rendered page for the code you’ll find below.

    If you don’t have a data.xml yet, the easiest way to start is to run this command in your presskit folder:

    presskit new -t company

    Then, open the generated data.xml file and fill the blanks while following this guide.

    Okay… ready? Let’s do this.


    For your products’ data.xml, refer to the product documentation.


  • Don’t forget to set your XML header correctly.

    <?xml version="1.0" encoding="utf-8"?>
  • Use a <company> tag for your main data.xml file. It will be the root page of your presskit.

    <company>
  • General Information

  • Begin with your company name, location, and miscellaneous information.

      <title>Pizza Burger Studio</title>
      <based-in>Paris, France</based-in>
      <founding-date>February 6, 2014</founding-date>
      <website>https://github.com/pixelnest/presskit.html</website>
      <phone>+42 (3) 42 42 42 42 42</phone>
  • For the address, use as many lines as you want. Don’t forget to specify the country.

      <address>
        <line>42 street of Pixelnest</line>
        <line>35000 Rennes</line>
        <line>France</line>
      </address>
  • This section allows you to set your social networks links. You can put whatever you like.

    But Twitter and Facebook look like obvious choices. ;)

      <socials>
        <social>
          <name>Twitter @pizzaburgerstudio</name>
          <link>https://twitter.com/pizzaburgerstudio</link>
        </social>
        <social>
          <name>Facebook Page</name>
          <link>https://facebook.com/pizzaburgerstudio</link>
        </social>
        <social>
          <name>Skype</name>
          <link>callto:pizzaburgerstudio</link>
        </social>
      </socials>
  • This field is the primary mail address you want to give to the press.

      <press-contact>contact@pizzaburger.studio</press-contact>
  • Use this tag to set your Google analytics tracking code.

    It’s, of course, completely optional.

      <analytics>UA-42424242-0</analytics>
  • Description & History

  • The description is a simple short block to describe your company or studio. Be brief.

      <description>
        This is a fake presskit for a fake company called Pizza Burger Studio.
      </description>
  • Tell your story with the <history> tag.

    You have two possibilities here.

    • Use an <histories> block like in the example.
    • Or use a single <history> block if you don’t have much to say.

    You can’t have both.

    Example of a single block:

    <history>
    Hello, world.
    </history>
      <histories>
        <history>
          <header>Built with presskit.html</header>
          <text>This presskit is generated by presskit.html, a tool created by Pixelnest Studio.</text>
        </history>
        <history>
          <header>Free and Open Source</header>
          <text>Find presskit.html on https://github.com/pixelnest/presskit.html</text>
        </history>
      </histories>
  • Trailers

  • Add your videos. Because we are in the <company> page, try to be representative of all the products of your company.

    You can use YouTube and/or Vimeo. Only one is needed, and only one is recommended.

    Don’t put the full link: just the ID is necessary.

    If you want to provide a link to download the video, add this tag in <trailer>:

    <download>https://example.com/video.mp4</download>
      <trailers>
        <trailer>
          <name>Example from YouTube. Don't give a full link: the ID is enough</name>
          <youtube>er416Ad3R1g</youtube>
        </trailer>
        <trailer>
          <name>Example from both YouTube and Vimeo</name>
          <youtube>YH3c1QZzRK4</youtube>
          <vimeo>108650530</vimeo>
        </trailer>
  • You can also add a video link without YouTube or Vimeo.

    In this case, only a sentence will be shown, along with a download link.

        <trailer>
          <name>Interview with Damien Mayance and Matthieu Oger of Pixenest Studio</name>
          <download>https://github.com/pixelnest/presskit.html</download>
        </trailer>
      </trailers>
  • You got awards? Great! Put them here.

    But don’t add too much here: this is your company page, not a product one.

      <awards>
        <award>
          <description>An award</description>
          <info>Name, location, 29 April, 1988</info>
        </award>
        <award>
          <description>A nomination</description>
          <info>Name, location, 03 October, 1988</info>
        </award>
      </awards>
  • Quotes are used to show the appreciation of your users.

    Show something important, something nice, or something funny, for example.

      <quotes>
        <quote>
          <description>There's not enough pizza in your life. It's never enough.</description>
          <name>@mrhelmut</name>
          <website>Tweet</website>
          <link>
            https://twitter.com/mrhelmut/status/717276362814447616
          </link>
        </quote>
      </quotes>
  • Need to link to a resource? A small product? An RSS feed?

    You want to show your blog? Your personal Twitter account?

    Put those here, in the <additionals> tag.

      <additionals>
        <additional>
          <title>Patricia Pizza Twitter</title>
          <description>@patpiz at</description>
          <link>https://twitter.com/patpiz</link>
        </additional>
        <additional>
          <title>Bob Burger Twitter</title>
          <description>@bobburg at</description>
          <link>https://twitter.com/bobburg</link>
        </additional>
      </additionals>
  • Team & Contacts

  • This is for your people.

    Add yourself, of course, but also your frequent collaborators for example.

    If a collaborator has only worked on one product, add her/him on this product page instead.

    The <role> tag can be anything you want. But we recommend to, at least, put the company name for the founders/employees. This will distinguish this person from the external collaborators.

    The <website> tag is optional.

      <credits>
        <credit>
          <person>Patricia Pizza</person>
          <role>Founder, Developer, Pizza Burger Studio</role>
        </credit>
        <credit>
          <person>Bob Burger</person>
          <role>Founder, Designer, Pizza Burger Studio</role>
        </credit>
        <credit>
          <person>Jake Burgza</person>
          <role>Artist, Collaborator</role>
          <website>
            http://www.jakeburgza.com
          </website>
        </credit>
        <credit>
          <person>Sophia Pizer</person>
          <role>Musician, Collaborator</role>
          <website>
            http://www.sophiapizer.com
          </website>
        </credit>
      </credits>
  • Add more contact information.

    For the company page, you can repeat your social network accounts, as well as another contact mail address, or your company website.

    For each item, pick one between a <link> and <mail> tag.

      <contacts>
        <contact>
          <name>Inquiries</name>
          <mail>contact@pizzaburger.studio</mail>
        </contact>
        <contact>
          <name>Twitter</name>
          <link>
            https://twitter.com/pizzaburgerstudio
          </link>
        </contact>
        <contact>
          <name>Facebook</name>
          <link>
            https://facebook.com/pizzaburgerstudio
          </link>
        </contact>
        <contact>
          <name>Web</name>
          <link>
            http://pizzaburger.studio
          </link>
        </contact>
      </contacts>
  • Well-done, it’s over for this one. ;)

    It’s time to do the product pages.

    </company>
================================================ FILE: docs/css/docco.css ================================================ /* * This is a modified version of the `docco` classic CSS. * https://github.com/jashkenas/docco/blob/master/resources/classic/docco.css */ /* ---------------------------------------------------------- *\ * Layout and Typography. \* ---------------------------------------------------------- */ html { height: 100%; font: 62.5%/1.6 "Trebuchet MS", Arial, Helvetica, sans-serif; } body { height: 100%; margin: 0; padding: 0; color: #252519; font-size: 1.3em; } #container { min-height: 100%; } a { color: #261a3b; } a:visited { color: #261a3b; } p, ul, ol { margin: 0 0 15px; } h1, h2, h3, h4, h5, h6 { margin: 30px 0 15px 0; font-family: Georgia, "Times New Roman", Times, serif; } h1 { margin-top: 40px; } hr { height: 1px; margin: 20px 0; border: 0 none; border-top: 1px solid #e5e5ee; } pre { white-space: pre-wrap; } pre, tt, code { margin: 0; padding: 0; font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; font-size: 12px; line-height: 16px; } ul.sections { margin: 0; padding: 0 0 5px 0; list-style: none;; } .annotation code { padding: 0 0.2em; background: #f5faff; border: 1px solid #dedede; font-size: 12px; } .annotation pre { margin-bottom: 15px; } .annotation pre code { padding: 5px 10px; } /* Force border-box so that % widths fit the parent container without overlap because of margin/padding. More Info : http://www.quirksmode.org/css/box.html */ ul.sections > li > div { -webkit-box-sizing: border-box; /* webkit */ -moz-box-sizing: border-box; /* firefox */ box-sizing: border-box; /* css3 */ -khtml-box-sizing: border-box; /* konqueror */ -ms-box-sizing: border-box; /* ie */ } /* ---------------------------------------------------------- *\ * "Jump Page" widget. \* ---------------------------------------------------------- */ #jump_to, #jump_page { margin: 0; list-style: none; background: white; -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; -webkit-box-shadow: 0 0 25px #777777; -moz-box-shadow: 0 0 25px #777777; font: 16px Arial; cursor: pointer; text-align: right; } #jump_to a { text-decoration: none; } #jump_to a.large { display: none; } #jump_to a.small { color: #676767; font-size: 22px; font-weight: bold; } #jump_to, #jump_wrapper { position: fixed; top: 0; right: 0; margin: 0; padding: 10px 15px; } #jump_wrapper { display: none; padding: 0; } #jump_to:hover #jump_wrapper { display: block; } #jump_page { margin: 0 0 25px 25px; padding: 5px 0 3px; } #jump_page .source { display: block; padding: 15px; border-top: 1px solid #eeeeee; text-decoration: none; } #jump_page .source:hover { background: #f5faff; } #jump_page .source:first-child { } /* ---------------------------------------------------------- *\ * Low resolutions (> 320px) \* ---------------------------------------------------------- */ @media only screen and (min-width: 320px) { .pilwrap { display: none; } ul.sections > li > div { display: block; padding: 5px 10px 0 10px; } ul.sections > li > div.annotation { background: #ffffff; } ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { padding-left: 30px; } ul.sections > li > div.content { overflow-x: auto; margin: 5px 10px 5px 10px; padding-bottom: 5px; background: #f5faff; border: 1px solid #dedede; -webkit-box-shadow: inset 0 0 5px #e5e5ee; box-shadow: inset 0 0 5px #e5e5ee; } } /* ---------------------------------------------------------- *\ * (> 750px) * * Change layout from one-column to two-columns. \* ---------------------------------------------------------- */ @media only screen and (min-width: 750px) { #container { position: relative; } body { background-color: #f5faff; line-height: 22px; } pre, tt, code { line-height: 18px; } #jump_to { padding: 5px 10px; } #jump_wrapper { padding: 0; } #jump_to, #jump_page { font: 10px Arial; text-transform: uppercase; } #jump_page .source { padding: 5px 10px; } #jump_to a.large { display: inline-block; } #jump_to a.small { display: none; } #background { position: absolute; z-index: -1; top: 0; bottom: 0; width: 350px; background: #ffffff; border-right: 1px solid #e5e5ee; } ul.sections > li > div.content { width: calc(95% - 350px); } ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { padding-left: 40px; } ul.sections > li { white-space: nowrap; } ul.sections > li > div { display: inline-block; } ul.sections > li > div.annotation { overflow-x: hidden; min-width: 350px; max-width: 350px; min-height: 5px; padding: 13px; text-align: left; vertical-align: top; white-space: normal; } ul.sections > li > div.content { padding: 13px; background: #f5faff; border: none; -webkit-box-shadow: none; box-shadow: none; vertical-align: top; } .pilwrap { position: relative; display: inline; } .pilcrow { position: absolute; top: 3px; left: -20px; padding: 1px 2px; opacity: 0; color: #454545; font: 12px Arial; -webkit-transition: opacity 0.2s linear; text-decoration: none; } .for-h1 .pilcrow { top: 47px; } .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { top: 35px; } ul.sections > li > div.annotation:hover .pilcrow { opacity: 1; } } /* ---------------------------------------------------------- *\ * (> 1025px) * * Add more spaces. \* ---------------------------------------------------------- */ @media only screen and (min-width: 1025px) { #background { width: 525px; } ul.sections > li > div.annotation { min-width: 525px; max-width: 525px; padding: 10px 25px 1px 50px; } ul.sections > li > div.content { width: calc(95% - 525px); padding: 9px 15px 16px 25px; } } /* ---------------------------------------------------------- *\ * Syntax Highlighting. \* ---------------------------------------------------------- */ /* The syntax highlighter used is https://highlightjs.org */ td.linenos { padding-right: 10px; background-color: #f0f0f0; } span.lineno { padding: 0 5px 0 5px; background-color: #f0f0f0; } /* github.com style (c) Vasily Polovnyov */ pre code { display: block; padding: 0.5em; color: #000000; background: #f5faff; } pre .hljs-comment, pre .hljs-template_comment, pre .hljs-diff .hljs-header, pre .hljs-javadoc { color: #408080; font-style: italic; } pre .hljs-keyword, pre .hljs-assignment, pre .hljs-literal, pre .hljs-css .hljs-rule .hljs-keyword, pre .hljs-winutils, pre .hljs-javascript .hljs-title, pre .hljs-lisp .hljs-title, pre .hljs-subst { color: #954121; /*font-weight: bold*/ } pre .hljs-number, pre .hljs-hexcolor { color: #40a070; } pre .hljs-string, pre .hljs-tag .hljs-value, pre .hljs-phpdoc, pre .hljs-tex .hljs-formula { color: #219161; } pre .hljs-title, pre .hljs-id { color: #19469d; } pre .hljs-params { color: #0000ff; } pre .hljs-javascript .hljs-title, pre .hljs-lisp .hljs-title, pre .hljs-subst { font-weight: normal; } pre .hljs-class .hljs-title, pre .hljs-haskell .hljs-label, pre .hljs-tex .hljs-command { color: #445588; font-weight: bold; } pre .hljs-tag, pre .hljs-tag .hljs-title, pre .hljs-rules .hljs-property, pre .hljs-django .hljs-tag .hljs-keyword { color: #000080; font-weight: normal; } pre .hljs-attribute, pre .hljs-variable, pre .hljs-instancevar, pre .hljs-lisp .hljs-body { color: #008080; } pre .hljs-regexp { color: #bb6688; } pre .hljs-class { color: #445588; font-weight: bold; } pre .hljs-symbol, pre .hljs-ruby .hljs-symbol .hljs-string, pre .hljs-ruby .hljs-symbol .hljs-keyword, pre .hljs-ruby .hljs-symbol .hljs-keymethods, pre .hljs-lisp .hljs-keyword, pre .hljs-tex .hljs-special, pre .hljs-input_number { color: #990073; } pre .hljs-builtin, pre .hljs-constructor, pre .hljs-built_in, pre .hljs-lisp .hljs-title { color: #0086b3; } pre .hljs-preprocessor, pre .hljs-pi, pre .hljs-doctype, pre .hljs-shebang, pre .hljs-cdata { color: #999999; font-weight: bold; } pre .hljs-deletion { background: #ffdddd; } pre .hljs-addition { background: #ddffdd; } pre .hljs-diff .hljs-change { background: #0086b3; } pre .hljs-chunk { color: #aaaaaa; } pre .hljs-tex .hljs-formula { opacity: 0.5; } ================================================ FILE: docs/example/css/master.css ================================================ @charset "UTF-8"; /* ---------------------------------------------------------- *\ * Globals. \* ---------------------------------------------------------- */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } body { position: relative; min-height: 100vh; margin: 0; } /* ---------------------------------------------------------- *\ * Typography & colors. \* ---------------------------------------------------------- */ html { color: #444444; font: 62.5%/1.6 "Trebuchet MS", Arial, Helvetica, sans-serif; } body { font-size: 1.26em; } h1, h2 { font-family: Georgia, "Times New Roman", Times, serif; font-weight: normal; } ::-moz-selection { color: white; background: #3398ff; } ::selection { color: white; background: #3398ff; } /* ---------------------------------------------------------- *\ * Basic elements. \* ---------------------------------------------------------- */ h1 { margin: 0; font-size: 2.8em; } h2 { margin: 25px 0 15px 0; font-size: 1.9em; } h3 { font-size: inherit; font-weight: bold; } img { max-width: 100%; } p { margin: 15px 0; } p:last-child { margin-bottom: 0; } /* Lists. */ ul, ol { margin: 15px 0; padding-left: 30px; } ul:last-child, ol:last-child { margin-bottom: 0; } ul { list-style-type: square; } dl { margin: 0; } dt { font-weight: bold; } dd { margin: 0; } dd + dt { margin-top: 15px; } /* ---------------------------------------------------------- *\ * Layout. \* ---------------------------------------------------------- */ body { padding: 20px 0; } .page { max-width: 1180px; margin: 0 auto; padding: 0 25px; } @media (min-width: 800px) { .page { display: -webkit-box; display: -ms-flexbox; display: flex; } .page-nav { min-width: 25%; } .page-wrapper { max-width: 75%; } .factsheet { min-width: 33%; } } /* ---------------------------------------------------------- *\ * Micro Components. \* ---------------------------------------------------------- */ .clearfix::after { display: table; clear: both; content: " "; } .capitalize { text-transform: capitalize; } /* ---------------------------------------------------------- *\ * Components - Links. \* ---------------------------------------------------------- */ a, a:visited { color: #0077dd; text-decoration: none; } a:hover { color: #005599; text-decoration: underline; } a:focus { outline: thin dotted; } a:active, a:hover { outline: none; } /* ---------------------------------------------------------- *\ * Components - Page Header. \* ---------------------------------------------------------- */ .page-header img { display: block; } /* ---------------------------------------------------------- *\ * Components - Nav. \* ---------------------------------------------------------- */ .nav { margin-bottom: 30px; } .nav__title { padding: 0 15px; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; line-height: 1; } .nav__title a, .nav__title a:visited, .nav__title a:hover { color: inherit; text-decoration: none; } .nav__subtitle { padding: 5px 15px; } .nav__list { padding: 0; list-style: none; } .nav__item, .nav__item:visited { display: block; padding: 5px 15px; color: inherit; } .nav__item:hover { color: inherit; background: rgba(0, 0, 0, 0.05); text-decoration: none; } .nav__item:focus { background: rgba(0, 0, 0, 0.05); outline: none; } /* ---------------------------------------------------------- *\ * Nav - Hamburger. \* ---------------------------------------------------------- */ .nav__toggle { display: none; margin: 15px 0; padding: 0 15px; } .nav__toggle .button { display: block; height: 30px; } /* Nav list variant for hamburger feature. */ .nav__list--slider { overflow-y: hidden; max-height: 0; -webkit-transition: max-height 0.5s ease-in-out; -moz-transition: max-height 0.5s ease-in-out; -o-transition: max-height 0.5s ease-in-out; transition: max-height 0.5s ease-in-out; } @media (min-width: 800px) { .nav__toggle { /* Always hide the button on big screens. */ display: none !important; } .nav__list--slider { /* Always show the list on big screens. So disable slider. */ max-height: unset !important; } } /* ---------------------------------------------------------- *\ * Components - Block. \* ---------------------------------------------------------- */ .block { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid rgb(221, 221, 221); } .block__notice { margin-bottom: 15px; padding: 10px; color: #027194; background: #ebf7fd; border: 1px solid rgba(45,112,145,0.3); border-radius: 4px; } .block__notice a { color: inherit; } /* ---------------------------------------------------------- *\ * Components - Grid. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .grid { display: -webkit-box; display: -ms-flexbox; display: flex; } .grid__item:not(:first-child) { padding-left: 25px; } .grid__item--flexible { -webkit-box-flex: 1; -ms-flex: 1; flex: 1; } } /* ---------------------------------------------------------- *\ * Components - Images Gallery. \* ---------------------------------------------------------- */ .gallery { margin-left: -25px; font-size: 0; /* Disable the small space below the images. */ } .gallery__item { float: left; width: 100%; padding-bottom: 25px; padding-left: 25px; } .gallery__item img { width: 100%; } .gallery__item a[href$=".gif"] { position: relative; display: block; } .gallery__item a[href$=".gif"]:focus:active { outline: none; } .gallery__item a[href$=".gif"]::after { position: absolute; z-index: 1; top: 0; left: 0; display: none; width: 100%; height: 100%; padding: 20px; background: rgba(0, 0, 0, 0.25); content: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAAABGdBTUEAALGPC/xhBQAAAFFQTFRFAAAA/////////////////////////////Pz8/Pz8/Pz8/f39/f39/f39/f39/f39/f39/Pz8/Pz8/Pz8/Pz8/v7+/f39/f39/f39/f39/f395zF2nQAAABt0Uk5TAAEJCxQeIEpXYGFnc3V2d5WarK2/xNLb3N3m8ZFI2gAAAOlJREFUOMuVlckOgzAMRAdIWUrKUgK4/v8P7QWVLA64c0RPY9mxByBQ3U/LSrQuU18jKzM49uQGI2KVJY5Etkq5ZmNBWxNhheWMbOFz5cxZzaXnd8Exz6en5UvZXx98o6OjarsDtyop/Hrnixt/zm3x/AggGQCj/6UFHpLpAMBFICRTB9Qcg6JpjV4ABdMekwSmphMWGUTRBaYL1gwYma6gLBiYEvYr8HTZ9aXzzVDYjHo88sA7SgaufkJhKRI7Zgdg0K5ZtLiC3bG46lPQH5f6XPUBoI8UdUjpY08fpH9EMwAzqsL+5vfxBcKnb1vURTbUAAAAAElFTkSuQmCC"); align-items: center; justify-content: center; } .gallery__item a[href$=".gif"].show-overlay::after { display: flex; } @media (min-width: 500px) { .gallery__item { width: 50%; } } /* ---------------------------------------------------------- *\ * Components - Logo. \* ---------------------------------------------------------- */ @media (min-width: 800px) { .logo { max-width: 49%; } } /* ---------------------------------------------------------- *\ * Components - Video player. \* ---------------------------------------------------------- */ .video-player { position: relative; padding-bottom: 56.25%; /* 16:9 */ } .video-player__frame { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } /* ---------------------------------------------------------- *\ * Components - Quote. \* ---------------------------------------------------------- */ .quote__content { margin: 0; } .quote__content::before { content: open-quote; } .quote__content::after { content: close-quote; } .quote__author::before { content: "-"; } /* ---------------------------------------------------------- *\ * Components - Button. \* ---------------------------------------------------------- */ .button { display: inline-block; padding: 5px 10px; background: -webkit-linear-gradient(top, white, #fafafa); background: linear-gradient(to bottom, white, #fafafa); border: 1px solid #cccccc; border-radius: 5px; } .button:hover { cursor: pointer; text-decoration: none; } .button:focus { border: 1px solid #3398ff; outline: none; } .button:active { color: white; background: -webkit-linear-gradient(top, #3398ff, #0077dd); background: linear-gradient(to bottom, #3398ff, #0077dd); } /* ---------------------------------------------------------- *\ * Widgets. \* ---------------------------------------------------------- */ .widget { width: 100%; } .widget--steam { height: 190px; } .widget--humble { height: 328px; } @media (max-width: 505px) { .widget--humble { height: 205px; } } .widget--itch { height: 167px; } .widget--gamejolt { height: 245px; } .widget--bandcamp { width: 100%; height: 120px; border: 0; } /* ---------------------------------------------------------- *\ * Hacks. \* ---------------------------------------------------------- */ @media (min-width: 800px) { ._team-fix-margin-top { margin-top: -15px; } } ================================================ FILE: docs/example/css/normalize.css ================================================ /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ /** * 1. Change the default font family in all browsers (opinionated). * 2. Correct the line height in all browsers. * 3. Prevent adjustments of font size after orientation changes in * IE on Windows Phone and in iOS. */ /* Document ========================================================================== */ html { font-family: sans-serif; /* 1 */ line-height: 1.15; /* 2 */ -ms-text-size-adjust: 100%; /* 3 */ -webkit-text-size-adjust: 100%; /* 3 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers (opinionated). */ body { margin: 0; } /** * Add the correct display in IE 9-. */ article, aside, footer, header, nav, section { display: block; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * Add the correct display in IE 9-. * 1. Add the correct display in IE. */ figcaption, figure, main { /* 1 */ display: block; } /** * Add the correct margin in IE 8. */ figure { margin: 1em 40px; } /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * 1. Remove the gray background on active links in IE 10. * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. */ a { background-color: transparent; /* 1 */ -webkit-text-decoration-skip: objects; /* 2 */ } /** * Remove the outline on focused links when they are also active or hovered * in all browsers (opinionated). */ a:active, a:hover { outline-width: 0; } /** * 1. Remove the bottom border in Firefox 39-. * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { border-bottom: none; /* 1 */ text-decoration: underline; /* 2 */ text-decoration: underline dotted; /* 2 */ } /** * Prevent the duplicate application of `bolder` by the next rule in Safari 6. */ b, strong { font-weight: inherit; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font style in Android 4.3-. */ dfn { font-style: italic; } /** * Add the correct background and color in IE 9-. */ mark { background-color: #ff0; color: #000; } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in * all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Embedded content ========================================================================== */ /** * Add the correct display in IE 9-. */ audio, video { display: inline-block; } /** * Add the correct display in iOS 4-7. */ audio:not([controls]) { display: none; height: 0; } /** * Remove the border on images inside links in IE 10-. */ img { border-style: none; } /** * Hide the overflow in IE. */ svg:not(:root) { overflow: hidden; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers (opinionated). * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: sans-serif; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Show the overflow in IE. * 1. Show the overflow in Edge. */ button, input { /* 1 */ overflow: visible; } /** * Remove the inheritance of text transform in Edge, Firefox, and IE. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` * controls in Android 4. * 2. Correct the inability to style clickable types in iOS and Safari. */ button, html [type="button"], /* 1 */ [type="reset"], [type="submit"] { -webkit-appearance: button; /* 2 */ } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } /** * Change the border, margin, and padding in all browsers (opinionated). */ fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } /** * 1. Correct the text wrapping in Edge and IE. * 2. Correct the color inheritance from `fieldset` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out * `fieldset` elements in all browsers. */ legend { box-sizing: border-box; /* 1 */ color: inherit; /* 2 */ display: table; /* 1 */ max-width: 100%; /* 1 */ padding: 0; /* 3 */ white-space: normal; /* 1 */ } /** * 1. Add the correct display in IE 9-. * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. */ progress { display: inline-block; /* 1 */ vertical-align: baseline; /* 2 */ } /** * Remove the default vertical scrollbar in IE. */ textarea { overflow: auto; } /** * 1. Add the correct box sizing in IE 10-. * 2. Remove the padding in IE 10-. */ [type="checkbox"], [type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Correct the cursor style of increment and decrement buttons in Chrome. */ [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. */ [type="search"]::-webkit-search-cancel-button, [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in IE 9-. * 1. Add the correct display in Edge, IE, and Firefox. */ details, /* 1 */ menu { display: block; } /* * Add the correct display in all browsers. */ summary { display: list-item; } /* Scripting ========================================================================== */ /** * Add the correct display in IE 9-. */ canvas { display: inline-block; } /** * Add the correct display in IE. */ template { display: none; } /* Hidden ========================================================================== */ /** * Add the correct display in IE 10-. */ [hidden] { display: none; } ================================================ FILE: docs/example/index.html ================================================ Pizza Burger Studio

Description

This is a fake presskit for a fake company called Pizza Burger Studio.

History

Built with presskit.html

This presskit is generated by presskit.html, a tool created by Pixelnest Studio.

Free and Open Source

Find presskit.html on https://github.com/pixelnest/presskit.html

Projects

Videos

Example from YouTube. Don't give a full link: the ID is enoughYouTube

Example from both YouTube and VimeoYouTube

Example from both YouTube and VimeoVimeo

Interview with Damien Mayance and Matthieu Oger of Pixenest StudioDownload

Images

Awards & Recognition

  • "An award" Name, location, 29 April, 1988
  • "A nomination" Name, location, 03 October, 1988

Selected Articles

  • There's not enough pizza in your life. It's never enough.
    @mrhelmut, Tweet
Patricia Pizza Twitter
@patpiz at twitter.com.
Bob Burger Twitter
@bobburg at twitter.com.
================================================ FILE: docs/example/js/hamburger.js ================================================ 'use strict' ;(function () { // Preamble: this code is very specific, and clearly made for then // main nav. // Currently, this functionality is not need elsewhere, so we didn't // bother to generalized this. // No guards too. // To disable this feature, just remove the script tag. window.addEventListener('DOMContentLoaded', function () { const container = document.querySelector('#hamburger') const button = document.querySelector('#hamburger-toggle') const target = document.querySelector('#hamburger-target') // Show hamburger (hidden by default if no js or hamburger disabled). container.style.display = 'block' // Get target height. const baseHeight = getElementHeight(target) // Do that after the height calculation! target.className += ' nav__list--slider' button.addEventListener('click', function (e) { e.preventDefault() e.stopPropagation() const currentMaxHeight = parseInt(target.style.maxHeight, 10) // If not set or 0, toggle to full height. // Otherwise, hide. if (!currentMaxHeight || currentMaxHeight === 0) { target.style.cssText = 'max-height: ' + baseHeight + 'px' } else { target.style.cssText = 'max-height: 0px' } }) }) // Clone an element outside the screen to safely get its height, then destroy it. function getElementHeight (element) { const clone = element.cloneNode(true) clone.style.cssText = 'visibility: hidden; display: block; margin: -999px 0' const height = (element.parentNode.appendChild(clone)).clientHeight element.parentNode.removeChild(clone) return height } })() ================================================ FILE: docs/example/product/index.html ================================================ My Super Game - Pizza Burger Studio

Description

Here goes a quick description of your product or game. Be concise and explain in very few words the concept or the gameplay and why it's really cool and why everyone should use or play it.

History

Add some storytelling here. Not the scenario of your game, but rather some backgrounds behind the creation process: why are you making this game? Most projects starts with a cool story.

Features

  • List some "Key Sellings Points" to grab player's attention.
  • Don't be too generic ("pixel art graphics!"), don't be too pretentious ("most incredible game experience!").
  • Also, people like numbers, so you can add some (450 weapons, hundreds of levels, dozens of hours of playtime!).
  • Need ideas? Maybe explain some game modes?
  • It would be a nice place to say something about multiplayer, if you have some.
  • Have you translated your game? (You probably should btw.)

Videos

Release TrailerYouTube

Gameplay Video #2YouTube

Early Access TrailerYouTube

Short gameplay preview: Burger vs PizzaDownload

Images

Widgets

Awards & Recognition

  • "Game of the year without a doubt" Saint-Père-Marc-En-Poulet (France), 04 February, 2016
  • "Best soundtrack" A great game festival (World), 01 October, 2015
  • "Best MYGAMENGINE game" Deep into the woods (Forest), 31 March, 2014

Selected Articles

  • This is my favorite game of all time.
    Mum, At home
  • A very serious quote you're very proud of by someone you respect.
    Master, Master's website
  • 10/10 would play it again and again.
    A friendly anonymous Steam reviewer, Steam review
Original Soundtrack (OST)
Composed by an awesome musician. Listen for free, download for $3 at zandernoriega.bandcamp.com.
Release announcement
Announcement are exciting, so we usually make blog posts or news about it on pixelnest.io.

About Pizza Burger Studio

Boilerplate
This is a fake presskit for a fake company called Pizza Burger Studio.
More information
More information on Pizza Burger Studio, our logo & relevant media are available here.

About Pixelnest Studio

Boilerplate
Pixelnest Studio is a small French studio which creates games, websites and apps. They made Steredenn, a roguelike-shmup in big pixels. They are also behind presskit.html, which you are probably using if you are reading this.
More information
More information on Pixelnest Studio is available here.

About Pizza Oven LLC

Boilerplate
This is a fake company to illustrate the fact that you can have multiple about tags in your product page.

My Super Game Credits

Krokmou
Bot Leader, Game Designer, Pixelnest Studio
Hiccup
Developer, Pixelnest Studio
Astrid
Musician, Freelancer
================================================ FILE: docs/product/index.html ================================================ Product — presskit.html
  • Product — presskit.html

  • This is your product or game page. It should focus on ONLY one product. This is not a replacement for your product’s website, but a quick way for the press to know it faster.

    Check the actual rendered page for the code you’ll find below.

    If you don’t have a data.xml yet, the easiest way to start is to run this command in your presskit folder:

    presskit new -t product

    Then, open the generated data.xml file and fill the blanks while following this guide.


    For your company’ data.xml, refer to the company documentation.


  • Don’t forget to set your XML header correctly.

    <?xml version="1.0" encoding="utf-8"?>
  • Use a <product> or <game> tag.

    <product>
  • General Information

  • Begin with your product name, website, and miscellaneous information.

      <title>My Super Game</title>
      <website>http://pizzaburger.studio/mysupergame</website>
  • You can also add an URL to a request page for this product. A button will be displayed on the page.

      <press-copy-request>
        http://pizzaburger.studio/mysupergame/request/
      </press-copy-request>
  • If you have one or many publishers for this product, you can add them here.

    The <based-in> and <website> tags are optional.

      <partners>
        <partner>
          <type>Publisher</type>
          <title>Pixelnest Studio</title>
          <website>https://pixelnest.io/</website>
          <based-in>Rennes, France</based-in>
        </partner>
        <partner>
          <type>Distributor</type>
          <title>Pizza Oven LLC</title>
        </partner>
      </partners>
  • Add as many dates, platforms and prices as needed.

      <release-dates>
        <release-date>04 Feb, 2016</release-date>
        <release-date>10 Oct, 2016</release-date>
      </release-dates>
  • Put all the platforms your product is available on.

    <link> tag is optional.

      <platforms>
        <platform>
          <name>PC / Mac</name>
          <link>http://itch.io/</link>
        </platform>
        <platform>
          <name>Steam</name>
          <link>http://steampowered.com/</link>
        </platform>
        <platform>
          <name>Inc New Store (TBA)</name>
        </platform>
      </platforms>
  • Show the price of your product. Try to be exhaustive, it will be helpful for a reviewer.

      <prices>
        <price>
          <currency>EUR</currency>
          <value>20</value>
        </price>
        <price>
          <currency>USD</currency>
          <value>20</value>
        </price>
        <price>
          <currency>GBP</currency>
          <value>16</value>
        </price>
        <price>
          <currency>JPY</currency>
          <value>2300</value>
        </price>
      </prices>
  • Relations

  • You can specify relations between products using the <relations> tag.

    Then, on this page product, you will see a new line in the factsheet:

    DLC:
    My Super Game: Ultimate Edition

    Of course, the Product Name value is clickable.

    In the related product page, a new value will also be added:

    DLC of:
    My Super Game

    Obviously, since the name we provide in the example is not a real product (ie., the product data.xml does not exist), this tag will be ignored for this page.

    You can have as many relations as you want. You can use it to show DLCs, expansions, sequels, prequels, etc.

    Warning: you need to rebuild the presskit to see the changes.

      <relations>
        <relation>
          <type>DLC</type>
          <product>My Super Game: Ultimate Edition</product>
        </relation>
      </relations>
  • Description & History

  • Describe your product here. Be brief.

      <description>
        Here goes a quick description of your product or game. Be concise and explain in very few words the concept or the gameplay and why it's really cool and why everyone should use or play it.
      </description>
  • Tell the story of your product with the <history> tag.

    You have two possibilities here.

    • Use an <history> block like in the example.
    • Or use an <histories> block if you have tons of things to say.

    You can’t have both.

    You can see an example of the <histories> block in the company page.

      <history>
        Add some storytelling here. Not the scenario of your game, but rather some backgrounds behind the creation process: why are you making this game? Most projects starts with a cool story.
      </history>
  • Features

  • Show the most important features of your product.

    Focus on what is really important and is a key aspect of it.

      <features>
        <feature>
          List some "Key Sellings Points" to grab player's attention.
        </feature>
        <feature>
          Don't be too generic ("pixel art graphics!"), don't be too pretentious ("most incredible game experience!").
        </feature>
        <feature>
          Also, people like numbers, so you can add some (450 weapons, hundreds of levels, dozens of hours of playtime!).
        </feature>
        <feature>
          Need ideas? Maybe explain some game modes?
        </feature>
        <feature>
          It would be a nice place to say something about multiplayer, if you have some.
        </feature>
        <feature>
          Have you translated your game? (You probably should btw.)
        </feature>
      </features>
  • Trailers

  • Add your videos. Trailer, teaser, snippets? Let’s roll!

    You can use YouTube and/or Vimeo. Only one is needed, and only one is recommended.

    Don’t put the full link: just the ID is necessary.

    If you want to provide a link to download the video, add this tag in <trailer>:

    <download>https://example.com/video.mp4</download>
      <trailers>
        <trailer>
          <name>Release Trailer</name>
          <youtube>EtXajayBLzw</youtube>
        </trailer>
        <trailer>
          <name>Gameplay Video #2</name>
          <youtube>EPNK1j3TMjU</youtube>
        </trailer>
        <trailer>
          <name>Early Access Trailer</name>
          <youtube>EtyQMcc19xY</youtube>
        </trailer>
        <trailer>
          <name>Short gameplay preview: Burger vs Pizza</name>
          <download>https://github.com/pixelnest/presskit.html</download>
        </trailer>
      </trailers>
  • Widgets

  • Add your widgets to your product here.

    If the product is not a game or an app, you won’t probably need that.

    We only need the ID of the product on the store.

    This section is optional, and you can use as many widgets (or none) as you want.

    Warning: widgets import many scripts and assets. This may have a penalty on your page size and responsiveness.

      <widgets>
        <appstore>950812012</appstore>
        <playstore>com.noodlecake.altosadventure</playstore>
        <steam>347160</steam>
        <humble>steredenn/7SDLfk23hw</humble>
        <itch>27992</itch>
        <bandcamp>1135613467</bandcamp>
      </widgets>
  • You got awards for this product? Great! Put them here.

    Optional.

      <awards>
        <award>
          <description>Game of the year without a doubt</description>
          <info>Saint-Père-Marc-En-Poulet (France), 04 February, 2016</info>
        </award>
        <award>
          <description>Best soundtrack</description>
          <info>A great game festival (World), 01 October, 2015</info>
        </award>
        <award>
          <description>Best MYGAMENGINE game</description>
          <info>Deep into the woods (Forest), 31 March, 2014</info>
        </award>
      </awards>
  • Quotes are used to show the appreciation of your users.

    Show something important, something nice, or something funny, for example.

    Optional.

      <quotes>
        <quote>
          <description>This is my favorite game of all time.</description>
          <name>Mum</name>
          <website>At home</website>
          <link>http://at.home/</link>
        </quote>
        <quote>
          <description>A very serious quote you're very proud of by someone you respect.</description>
          <name>Master</name>
          <website>Master's website</website>
          <link>http://mast.er/</link>
        </quote>
        <quote>
          <description>10/10 would play it again and again.</description>
          <name>A friendly anonymous Steam reviewer</name>
          <website>Steam review</website>
          <link>http://steam.review/</link>
        </quote>
      </quotes>
  • Need to link to a resource? A small product? An RSS feed? An OST?

    A press release?

    Put those here, in the <additionals> tag.

    Optional.

      <additionals>
        <additional>
          <title>Original Soundtrack (OST)</title>
          <description>Composed by an awesome musician. Listen for free, download for $3 at</description>
          <link>http://zandernoriega.bandcamp.com/album/steredenn-original-soundtrack</link>
        </additional>
        <additional>
          <title>Release announcement</title>
          <description>Announcement are exciting, so we usually make blog posts or news about it on</description>
          <link>http://pixelnest.io/journal/</link>
        </additional>
      </additionals>
  • About

  • An “About Your Company” section is automatically generated from the content of the company page.

    This tag is completely optional. If you don’t want this section, just delete it. But sometimes, you want to add a description for a partner.

    This is this the place to do it.

    For example, if you have a publisher, you can speak a bit more about it here.

    Most of the time, you should link that to the tag above, if you want to explain a partner a bit more. But you could use this section for other purpose.

    The tag is optional.

      <abouts>
        <about>
          <title>Pixelnest Studio</title>
          <description>Pixelnest Studio is a small French studio which creates games, websites and apps. They made Steredenn, a roguelike-shmup in big pixels. They are also behind presskit.html, which you are probably using if you are reading this.</description>
          <link>https://pixelnest.io/</link>
        </about>
        <about>
          <title>Pizza Oven LLC</title>
          <description>This is a fake company to illustrate the fact that you can have multiple about tags in your product page.</description>
        </about>
      </abouts>
  • Team & Contacts

  • This is for the product team.

    Add yourself, of course, but also your collaborators, freelancers and/or partners for example.

    The <role> tag can be anything you want. But we recommend to, at least, put the company name for the founders/employees. This will distinguish this person from the external collaborators.

    The <website> tag is optional.

      <credits>
        <credit>
          <person>Krokmou</person>
          <role>Bot Leader, Game Designer, Pixelnest Studio</role>
        </credit>
        <credit>
          <person>Hiccup</person>
          <role>Developer, Pixelnest Studio</role>
        </credit>
        <credit>
          <person>Astrid</person>
          <website>http://www.astridsupergame.com</website>
          <role>Musician, Freelancer</role>
        </credit>
      </credits>
  • Add more contact information.

    Add links to your product social accounts, as well as mail specific to this product. And the website.

    For each item, pick one between a <link> and <mail> tag.

      <contacts>
        <contact>
          <name>Inquiries</name>
          <mail>mysupergame@pizzaburger.studio</mail>
        </contact>
        <contact>
          <name>Twitter</name>
          <link>
            https://twitter.com/pizzaburgerstudio
          </link>
        </contact>
        <contact>
          <name>Facebook</name>
          <link>
            https://facebook.com/pizzaburgerstudio
          </link>
        </contact>
        <contact>
          <name>Web</name>
          <link>
            http://mysupergame.pizzaburger.studio/
          </link>
        </contact>
      </contacts>
  • All done! 🍾

    </product>
================================================ FILE: documentation ================================================ #!/usr/bin/env bash BASEDIR=$(dirname $0) cd $BASEDIR BIN=$(npm bin) $BIN/docco -l classic -o docs/company data/data.xml $BIN/docco -l classic -o docs/product data/product/data.xml # Use custom CSS. CSS_BASE='' CSS_MODIFIED='' sed "s#$CSS_BASE#$CSS_MODIFIED#" docs/company/data.html > docs/company/index.html sed "s#$CSS_BASE#$CSS_MODIFIED#" docs/product/data.html > docs/product/index.html # Clean. rm {docs/company/data.html,docs/product/data.html} rm {docs/company/docco.css,docs/product/docco.css} ================================================ FILE: lib/assets.js ================================================ 'use strict' const path = require('upath') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- const assets = path.join(__dirname, '../assets') const cssFolder = path.join(assets, 'css') const assetsToCopy = [ path.join(assets, 'css/normalize.css'), path.join(assets, 'css/print.css'), path.join(assets, 'js/hamburger.js'), path.join(assets, 'js/imagesloaded.min.js'), path.join(assets, 'js/masonry.min.js') ] const companyTemplate = path.join(assets, 'templates/company.xml') const productTemplate = path.join(assets, 'templates/product.xml') const imagesFolderName = 'images' const authorizedImageFormats = ['.jpg', '.jpeg', '.png', '.gif'] // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { assets, cssFolder, assetsToCopy, imagesFolderName, authorizedImageFormats, companyTemplate, productTemplate } ================================================ FILE: lib/config.js ================================================ 'use strict' // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- const config = { commands: { build: {}, new: {} } } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = config ================================================ FILE: lib/core/builder.js ================================================ 'use strict' const fs = require('fs') const path = require('upath') const chalk = require('chalk') const sharp = require('sharp') const packageVersion = require('../../package.json').version const console = require('../helpers/color-console') const sfs = require('../helpers/sfs') const zip = require('../helpers/zip') const { createTemplate } = require('./template') const config = require('../config') const { imagesFolderName, authorizedImageFormats } = require('../assets') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- async function build (dataFilePath, presskit, { company = {}, products = [], analytics = '' } = {}) { const buildFolder = createAndGetBuildFolder() const pageFolder = getPageFolder(buildFolder, dataFilePath, presskit) // Create the page folder. sfs.createDir(pageFolder) const htmlFilePath = getHtmlFilePath(pageFolder) console.log(`- "${presskit.title}" -> ${chalk.blue(htmlFilePath)}`) // Templates and images. const template = createTemplate(presskit.type, pageFolder) const assetsSource = getImagesFolder(dataFilePath) const images = getImages(assetsSource) // Copy images and zips to the page folder. const assetsTarget = path.join(pageFolder, imagesFolderName) sfs.copyDirContent(assetsSource, assetsTarget) // Add thumbnails on the screenshot images. // Because the thumbnails are not in the `images` object, they won't // be added to the zips. Neat. if (!config.commands.build.ignoreThumbnails) { await createThumbnails(assetsTarget, images.screenshots) } // This must be done after `sfs.copyDirContent`. // Otherwise, we might override existing archives. exportArchives(images, assetsSource, assetsTarget) // Apply data to template. const html = template({ presskit, company, products, images: sortScreenshotsByCategories(images), assets: (presskit.type === 'company' || presskit.topLevel ? '.' : '..'), analytics, // Handlebars can't check for equality, so we provide two // variables to differentiate a product and a company sheet. isCompany: presskit.type === 'company', isProduct: presskit.type === 'product', // Same to know if a presskit has any screenshots. hasScreenshots: images.screenshots && images.screenshots.length > 0, // Additional build options: hamburger: config.commands.build.hamburger, // Misc. buildVersion: packageVersion, buildTime: new Date().getTime() }) // Export the HTML to a file. fs.writeFileSync(htmlFilePath, html) // And return the relative path from `build/`. return path.relative(buildFolder, htmlFilePath) } // ------------------------------------------------------------- // Folders. // ------------------------------------------------------------- function createAndGetBuildFolder () { const destination = config.commands.build.output sfs.createDir(destination) return destination } function getHtmlFilePath (pageFolder) { return path.join(pageFolder, 'index.html') } function getPageFolder (buildFolder, dataFilePath, presskit) { // Logic: // - Company page should be placed in the root folder. // - Top-level products (standalone game presskits) go to the root folder too. // - But each other product page is a subfolder. if (presskit.type === 'company' || presskit.topLevel) { return buildFolder } const productFolderName = path.basename(path.dirname(dataFilePath)) return path.join(buildFolder, productFolderName) } // Get an absolute page url from its location in the build process. function getAbsolutePageUrl (dataFilePath, presskit) { const buildFolder = createAndGetBuildFolder() const pageFolder = getPageFolder(buildFolder, dataFilePath, presskit) const htmlFilePath = getHtmlFilePath(pageFolder) const relativePath = path.posix.relative(buildFolder, htmlFilePath) return path.posix.join(config.commands.build.baseUrl, relativePath) } // ------------------------------------------------------------- // Images. // ------------------------------------------------------------- // Get the folder containing the images. function getImagesFolder (dataFile) { const stat = fs.statSync(dataFile) const dataFileFolder = stat.isFile() ? path.dirname(dataFile) : dataFile return path.join(dataFileFolder, imagesFolderName) } // Get the images in the source, and sort them by types. // We are interested in three types of images: // - the header, starting with 'header'. // - the logos, starting with 'logo'. // - And the rest (what we call "screenshots"). function getImages (source) { if (!fs.existsSync(source)) { console.error(`No images found for "${source}"`, '🤔') return { header: null, screenshots: [], logos: [] } } const images = { header: null, screenshots: [], logos: [] } sfs.findAllFiles(source) .forEach(filePathWithSource => { // Remove 'path/to/images' from 'path/to/images/filename'. // ie, 'data/product/images/burger01.png' becomes 'burger01.png'. // This is super important. We only need this portion for: // - the comparison below // - the url on the site // - the copy to the built presskit const f = path.relative(source, filePathWithSource) // Authorize only these formats. const ext = path.extname(f) if (!authorizedImageFormats.includes(ext)) return // Ignore favicon. if (f.toLowerCase() === 'favicon.ico') return // And put in the correct category. if (f.toLowerCase().startsWith('header')) { images.header = f } else if (f.toLowerCase().startsWith('logo')) { images.logos.push(f) } else { images.screenshots.push(f) } }) return images } // This function takes a list of images and separate the images which are // in a folder into separate categories. // // **Pure function.** // // Example: // - burger01.png // - gifs/burger02.png // - wallpapers/burger03.png // - wallpapers/burger04.png // → { // ... // screenshots: ['burger01.png'] // screenshotsWithCategory: { // gifs: {title: 'gifs', elements: ['gifs/burger02.png']} // wallpapers: {title: 'gifs', elements: ['wallpapers/burger03.png', 'wallpapers/burger04.png']} // } // ... // } function sortScreenshotsByCategories (images) { const clone = { ...images } // Abort early if no screenshots. const screenshots = clone.screenshots if (!screenshots) return clone // We put category-less screenshots first. This is important. const result = [] const withCategories = {} for (const i of screenshots) { // We get the category from the dirname. const category = path.dirname(i) // Separate the screenshots which are at the root of the images folder. // They must be first, so we store them in another array. if (category === '.') { result.push(i) } else { // Create a new category first. if (!withCategories[category]) { withCategories[category] = {} withCategories[category].title = category withCategories[category].elements = [] } withCategories[category].elements.push(i) } } clone.screenshots = result clone.screenshotsWithCategory = withCategories return clone } // Create thumbnails. // For each provided images, generate a low-res jpg thumbnail. // Even for gif. // // We don't check if the format of the images is valid, // since the check is already done when constructing the images object. async function createThumbnails (target, images) { for (const x of images) { const imagePath = path.join(target, x) const thumbPath = path.join(target, `${x}.thumb.jpg`) try { // Background and flatten are for transparent PNGs/Gifs. // We force a white background here. await sharp(imagePath) .resize(450) .flatten({ background: { r: 255, g: 255, b: 255, alpha: 1 } }) .jpeg() .toFile(thumbPath) } catch (e) { const msg = chalk.dim(`(${e.message})`) console.warn(`The '${chalk.bold(x)}' thumbnail has not been generated. ${msg}`) } } } // ------------------------------------------------------------- // Archives. // ------------------------------------------------------------- // Export archives for screenshots and logos. function exportArchives (images, source, target) { if (!images) return createArchive('images.zip', target, source, images.screenshots) createArchive('logo.zip', target, source, images.logos) } function createArchive (name, target, source, files) { if (!files) return // Do not override an existing archive. if (fs.existsSync(path.join(source, name))) { console.warn(`An existing archive named ${chalk.blue(name)} has been found in the assets. Keeping it…`) return } zip(name, target, source, files) } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { build, getAbsolutePageUrl, createAndGetBuildFolder } ================================================ FILE: lib/core/generator.js ================================================ 'use strict' const { promisify } = require('util') const fs = require('fs') const readFilePromised = promisify(fs.readFile) const writeFilePromised = promisify(fs.writeFile) const path = require('upath') const chalk = require('chalk') const console = require('../helpers/color-console') const sfs = require('../helpers/sfs') const { installWatcher, installDevelopmentWatcher } = require('../helpers/watcher') const { build, createAndGetBuildFolder, getAbsolutePageUrl } = require('./builder') const loader = require('./loader') const { assetsToCopy, cssFolder } = require('../assets') const config = require('../config') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- async function generate (entryPoint) { console.log(`Starting directory: ${chalk.green(entryPoint)}`) cleanBuildFolder() const pages = findData(entryPoint) if (countDataFiles(pages) === 0) { console.log('') console.warn('No data files found!') return } await generateHTML(pages) await exportAdditionalAssets() console.log('') console.log('Done! 👌') if (config.commands.build.watch) { const port = config.commands.build.port const address = chalk.green(`http://localhost:${port}/`) showHeader(`Watching and serving files from ${address}`) startWatcher(entryPoint, () => generateHTML(findData(entryPoint)).catch(err => console.error(err.message))) } } function cleanBuildFolder () { if (!config.commands.build.cleanBuildFolder) return showHeader('Cleaning build folder') fs.rmSync(createAndGetBuildFolder(), { recursive: true, force: true }) } function findData (entryPoint) { showHeader('Finding data') const pages = { products: [] } // Inspect given path for data files and get all correct ones. const files = sfs.findAllFiles(entryPoint, { maxDepth: 1, ignoredFolders: ['build', 'node_modules'] }) files.filter(isDataFile).forEach(file => { try { const presskit = loader.loadDataFile(file) if (!presskit) return console.log(`- ${presskit.type}: "${presskit.title}" ${chalk.dim(file)}`) // Mark top-level products so they build to the root folder. const isTopLevel = path.relative(path.dirname(file), entryPoint) === '' if (isTopLevel && presskit.type !== 'company') { presskit.topLevel = true } if (presskit.type === 'company') { if (pages.company) { console.warn('Multiple companies detected. This is not supported yet, only the last will be used!') } pages.company = { path: file, presskit } } else { pages.products.push({ path: file, presskit }) } } catch (err) { console.error(`There was an error during the parsing of \`${file}\`. Check that your XML document is valid.`) console.error('You can use a validator like this: ' + chalk.blue('http://codebeautify.org/xmlvalidator')) process.exit(1) } }) return pages } async function generateHTML (pages) { showHeader('Generating HTML') const productsList = [] let analytics = '' if (pages.company && pages.company.presskit.analytics) { analytics = pages.company.presskit.analytics } // Create dependency relations between products. // ie., if a product is a DLC of another, it will add // this link between the two products. addProductsRelations(pages.products) for (const product of pages.products) { const outputPath = await build( product.path, product.presskit, { company: pages.company ? pages.company.presskit : {}, analytics } ) productsList.push({ path: outputPath, title: product.presskit.title }) } // Company should be the last one, listing all the products if (pages.company) { // Notice that we provide a ref to all the products here. await build( pages.company.path, pages.company.presskit, { products: productsList, analytics } ) } } function addProductsRelations (products) { // Find a tag in each product. for (const currentProduct of products) { const relations = currentProduct.presskit.relations if (!relations) continue // Erase the old relations object and make it an array for future use. currentProduct.presskit.relations = [] // Then go through each relation and add the links between the // current product and its target. relations.forEach(relation => { if (!relation.type || typeof relation.type !== 'string') return const type = relation.type.trim() const productId = cleanId(relation.product) // Find the target of the relation (ONE product per relation). const relatedProduct = products .filter(x => x !== currentProduct) .find(x => productId === cleanId(x.presskit.title)) // Stop if nothing. if (!relatedProduct) return // Then add the relations. // On the current product: currentProduct.presskit.relations.push( createRelation(type, relatedProduct) ) // And the target. if (!relatedProduct.presskit.relationOf) { relatedProduct.presskit.relationOf = [] } relatedProduct.presskit.relationOf.push(createRelation(type, currentProduct)) }) } function cleanId (x) { if (!x || typeof x !== 'string') return '' return x.trim().toLowerCase().replace(/\s/g, '') } // Helper to create a relation object. function createRelation (type, product) { return { type, text: product.presskit.title, path: getAbsolutePageUrl(product.path, product.presskit) } } } async function exportAdditionalAssets () { showHeader('Exporting assets') // Copy assets (CSS and stuff) await copyMandatoryFiles() // Copy the selected CSS theme. await copyThemeCss() } // Print a console UI header text. function showHeader (text) { console.log('') console.log(chalk.magenta(text + '…')) } // Add css and any mandatory files specified to the build directory async function copyMandatoryFiles () { const buildDir = createAndGetBuildFolder() for (const f of assetsToCopy) { const filepath = path.resolve(f) // Get filename and dirname from the provided path. const filename = path.basename(filepath) const dirname = path.basename(path.dirname(filepath)) // Create the directory for this file if needed. // ie. css/master.css needs a `css` directory. const targetDir = path.join(buildDir, dirname) sfs.createDir(targetDir) // And copy the file. const targetPath = path.join(targetDir, filename) console.log('- ' + targetPath) try { await writeFilePromised(targetPath, await readFilePromised(filepath)) } catch (e) { const msg = chalk.dim(`(${e.message})`) console.error(`There was an error while copying ${chalk.bold(filename)}. ${msg}`) } } } // Resolve and copy the CSS theme to the build directory as light.css. // If the value is a bare name (no path separator), resolve from the // built-in css folder (e.g. "dark" → assets/css/dark.css). // Otherwise, treat it as a file path. async function copyThemeCss () { const css = config.commands.build.css || 'light' // Bare name (no slash) → built-in theme. const isBuiltIn = !css.includes('/') && !css.includes('\\') const filepath = isBuiltIn ? path.join(cssFolder, css.endsWith('.css') ? css : `${css}.css`) : path.resolve(css) if (!fs.existsSync(filepath)) { console.error(`CSS file not found: ${chalk.blue(filepath)}`) return } const buildDir = createAndGetBuildFolder() const targetDir = path.join(buildDir, 'css') sfs.createDir(targetDir) const targetPath = path.join(targetDir, 'theme.css') console.log('- ' + targetPath) try { await writeFilePromised(targetPath, await readFilePromised(filepath)) } catch (e) { const msg = chalk.dim(`(${e.message})`) console.error(`There was an error while copying CSS theme. ${msg}`) } } // Count the number of products/company in the presskit. function countDataFiles (pages) { let count = 0 count += (pages.company ? 1 : 0) count += pages.products.length return count } // Is a file qualifying to be a data file? ie. `data.xml`. // We don't check the content here, just the name. function isDataFile (filename) { const ext = path.extname(filename) const filenameWithoutExt = path.basename(filename, ext) return filenameWithoutExt === 'data' && ext === '.xml' } // Start watching files and assets. function startWatcher (entryPoint, callback) { if (config.commands.build.dev) { // Delete the assets (CSS/etc.) before. // They might not be there, but we must ensure that they are properly // deleted. // // This is necessary, otherwise, remaining copied CSS files will have // an higher priority than the ones provided by the server (from // the `assets/` folder). fs.rmSync(path.join(createAndGetBuildFolder(), 'css'), { recursive: true, force: true }) installDevelopmentWatcher(entryPoint, callback) } else { installWatcher(entryPoint, callback) } } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { generate } ================================================ FILE: lib/core/generator.test.js ================================================ 'use strict' const fs = require('fs') const path = require('path') const os = require('os') const generator = require('./generator') const config = require('../config') // ------------------------------------------------------------- // Helpers. // ------------------------------------------------------------- function createTempDir () { return fs.mkdtempSync(path.join(os.tmpdir(), 'presskit-test-')) } function writeXML (dir, xml) { fs.writeFileSync(path.join(dir, 'data.xml'), xml) } function setupConfig (outputDir) { config.commands.build = { output: outputDir, ignoreThumbnails: true, baseUrl: '/', prettyLinks: false, hamburger: false } } const companyXML = ` Test Studio Paris 2020 https://example.com press@example.com A test studio. Test Person Developer ` const gameXML = ` Test Game https://example.com/game A test game. Test Person Developer ` const productXML = ` Test Product https://example.com/product A test product. Test Person Developer ` // ------------------------------------------------------------- // Tests. // ------------------------------------------------------------- describe('generate()', () => { let tempDir let buildDir beforeEach(() => { tempDir = createTempDir() buildDir = path.join(tempDir, 'build') setupConfig(buildDir) }) afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }) }) it('should build a company page', async () => { writeXML(tempDir, companyXML) await generator.generate(tempDir) expect(fs.existsSync(path.join(buildDir, 'index.html'))).toBe(true) }) it('should build a company with a product', async () => { writeXML(tempDir, companyXML) const productDir = path.join(tempDir, 'mygame') fs.mkdirSync(productDir) writeXML(productDir, productXML) await generator.generate(tempDir) expect(fs.existsSync(path.join(buildDir, 'index.html'))).toBe(true) expect(fs.existsSync(path.join(buildDir, 'mygame', 'index.html'))).toBe(true) }) it('should build a standalone game without a company', async () => { writeXML(tempDir, gameXML) await generator.generate(tempDir) expect(fs.existsSync(path.join(buildDir, 'index.html'))).toBe(true) }) it('should build a standalone product without a company', async () => { writeXML(tempDir, productXML) await generator.generate(tempDir) expect(fs.existsSync(path.join(buildDir, 'index.html'))).toBe(true) }) it('should not crash with no data files', async () => { await generator.generate(tempDir) expect(fs.existsSync(buildDir)).toBe(false) }) it('should include company info in product page when company exists', async () => { writeXML(tempDir, companyXML) const productDir = path.join(tempDir, 'mygame') fs.mkdirSync(productDir) writeXML(productDir, productXML) await generator.generate(tempDir) const html = fs.readFileSync(path.join(buildDir, 'mygame', 'index.html'), 'utf-8') expect(html).toContain('Test Studio') }) it('should not include company section in standalone game page', async () => { writeXML(tempDir, gameXML) await generator.generate(tempDir) const html = fs.readFileSync(path.join(buildDir, 'index.html'), 'utf-8') expect(html).not.toContain('About ') }) }) ================================================ FILE: lib/core/loader.js ================================================ 'use strict' const fs = require('fs') const parser = require('./parser') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- // The XML parser can't understand automatically that a list of // XML children is an array. // // So it generates aberrations like: // // "awards": { // "award": [ // {"description": ""}, // {"description": ""} // ] // } // // Moreover, if there's only one element, it will not put it in // an array. ie., if there's one award in the array above, the result // is going to be: // // "awards": { // "award": {"description": ""} // } // // But we want an array every time. // // The keys below are the arrays that need to be normalized. const keysToNormalize = [ // Remember, we convert kebab-case to camelCase, // so release-dates is converted to releaseDates. 'releaseDates', 'partners', 'platforms', 'prices', 'relations', 'features', 'socials', 'histories', 'trailers', 'awards', 'quotes', 'additionals', 'abouts', 'credits', 'contacts' ] const tagsToClean = [ '
', '
', '
', '
', '
', '
', '', '', '', '', '', '', '', '' ] // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- function loadDataFile (filename) { let xml = fs.readFileSync(filename, 'utf-8') // Remove inline tags. xml = cleanTokens(tagsToClean, xml) const rawJSON = parser.parseXML(xml) // Normalize some keys. return normalizeKeys(keysToNormalize, rawJSON) } function cleanTokens (tokens, str) { const regex = new RegExp(tokens.join('|'), 'gi') return str.replace(regex, '') } // TODO: this should probably be done as a custom tag processor with xml2js. function normalizeKeys (keys, json) { Object.keys(json) .filter(k => keys.includes(k)) .forEach(k => { json[k] = normalize(json[k]) }) return json } function normalize (obj) { const keys = Object.keys(obj) // Ignore if there're more than one key or 0 in the object. if (keys.length === 0 || keys.length > 1) return obj // Get the content of the only key of the object. const data = obj[keys[0]] // An array? Return it. if (Array.isArray(data)) { return data } // Otherwise, wrap in an array. return [data] } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { loadDataFile, __cleanTokens: cleanTokens, __normalizeKeys: normalizeKeys, __normalize: normalize } ================================================ FILE: lib/core/loader.test.js ================================================ 'use strict' const { __normalizeKeys: normalizeKeys, __normalize: normalize, __cleanTokens: cleanTokens } = require('./loader') // ------------------------------------------------------------- // Tests. // ------------------------------------------------------------- describe('cleanTokens()', () => { const tokens = [ '
', '
', '
', '', '', '', '' ] it('should not change a string without tokens', () => { expect(cleanTokens(tokens, 'test test test')).toEqual('test test test') }) it('should remove all tokens', () => { expect(cleanTokens(tokens, '
This is a
string')) .toEqual('This is a string') }) it('should remove all tokens with multiline string', () => { const start = `
This is a
string Yep, it is.

` const result = ` This is a string Yep, it is. ` expect(cleanTokens(tokens, start)).toEqual(result) }) }) describe('normalizeKeys()', () => { const keys = ['quotes', 'awards'] const received = { quotes: { quote: { description: 'quote' } }, awards: { award: [ { description: 1 }, { description: 2 } ] }, test: 42, socials: { social: [ { text: 1 }, { text: 2 } ] } } const correct = { quotes: [{ description: 'quote' }], awards: [ { description: 1 }, { description: 2 } ], test: 42, socials: { social: [ { text: 1 }, { text: 2 } ] } } it('normalizes a list of provided keys in an object', () => { expect(normalizeKeys(keys, received)).toEqual(correct) }) }) describe('normalize()', () => { it('returns the correct content as it is', () => { expect(normalize([ { description: 1 }, { description: 2 } ])).toEqual([ { description: 1 }, { description: 2 } ]) }) it('returns an object without the useless intermediate key', () => { expect(normalize({ award: [ { description: 1 }, { description: 2 } ] })).toEqual([ { description: 1 }, { description: 2 } ]) }) it('returns an object containing an array, even if there is one item only', () => { expect(normalize({ award: { description: 1 } })).toEqual([ { description: 1 } ]) }) it('ignores an object containing 0 key', () => { expect(normalize({})).toEqual({}) }) it('ignores an object containing 2 keys or more', () => { expect(normalize({ test: true, award: [ { description: 1 }, { description: 2 } ] })).toEqual({ test: true, award: [ { description: 1 }, { description: 2 } ] }) }) }) ================================================ FILE: lib/core/parser.js ================================================ 'use strict' const { Parser: XMLParser } = require('xml2js') const camelCase = require('camelcase') // ------------------------------------------------------------- // Constants. // ------------------------------------------------------------- const OPTIONS = { explicitArray: false, async: false, tagNameProcessors: [x => camelCase(x)] } // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- function parseXML (xml) { if (!xml) { throw new Error('XML input was null or empty') } let data new XMLParser(OPTIONS).parseString(xml, function (err, result) { if (err || !result) { throw new Error('Failed to parse XML file') } // Values are nested in the "product", "game" or "company" root node. // We need to detect and ignore this root element. if (result.game) { data = result.game data.type = 'product' } else if (result.product) { data = result.product data.type = 'product' } else if (result.company) { data = result.company data.type = 'company' } else { throw new Error('Unrecognized XML file, expected or root tag') } }) return data } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { parseXML } ================================================ FILE: lib/core/parser.test.js ================================================ 'use strict' const fs = require('fs') const parser = require('./parser') // ------------------------------------------------------------- // Tests. // ------------------------------------------------------------- describe('XML Parser', () => { const companyXML = fs.readFileSync(`${process.cwd()}/data/data.xml`, 'utf-8') const productXML = fs.readFileSync(`${process.cwd()}/data/product/data.xml`, 'utf-8') it('handles empty, null or undefined XML strings', () => { expect(() => parser.parseXML(undefined)).toThrow() expect(() => parser.parseXML(null)).toThrow() expect(() => parser.parseXML('')).toThrow() }) it('handles invalid XML strings', () => { expect(() => parser.parseXML('Test. This is not XML')).toThrow() }) it('handles incomplete XML strings', () => { const data = 'Gambardella, Matthew' expect(() => parser.parseXML(data)).toThrow() }) it('handles valid XML strings but invalid presskit data', () => { const data = 'Gambardella, MatthewXML Developer\'s GuideComputer44.952000-10-01An in-depth look at creating applications with XML.' expect(() => parser.parseXML(data)).toThrow() }) it('handles valid XML `company` presskit string', () => { const result = parser.parseXML(companyXML) expect(result.type).toBe('company') expect(result.title).toBeDefined() expect(result.description).toBeDefined() }) it('handles valid XML `product` or `game` presskit string', () => { const result = parser.parseXML(productXML) expect(result.type).toBe('product') expect(result.title).toBeDefined() expect(result.description).toBeDefined() }) }) ================================================ FILE: lib/core/template.js ================================================ 'use strict' const fs = require('fs') const path = require('upath') const handlebars = require('handlebars') const config = require('../config') const { assets } = require('../assets') const console = require('../helpers/color-console') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- // Create a template object from a template of the assets folder. // Use the type to determine which template must be selected. function createTemplate (type, destination) { const templatePath = getTemplatePath(assets, type) if (!templatePath) { throw new Error('Missing template! Make sure your presskit has a "type" field (product/company)') } registerPartials(assets) registerHelpers(destination) const template = fs.readFileSync(templatePath, 'utf-8') return handlebars.compile(template) } // Get the path to the corresponding template in the assets folder. function getTemplatePath (folder, type) { switch (type) { case 'product': return path.join(folder, 'product.html') case 'company': return path.join(folder, 'company.html') } } // Add all the required partials. function registerPartials (folder) { const partialsFolder = path.join(folder, '_includes') const partials = fs.readdirSync(partialsFolder) .filter(isValidPartial) .reduce((result, partial) => { const ext = path.extname(partial) const fileFullPath = path.join(partialsFolder, partial) const data = fs.readFileSync(fileFullPath, 'utf-8') // Store as `"filename without extension": content`. result[path.basename(partial, ext)] = data return result }, {}) handlebars.registerPartial(partials) } function registerHelpers (destination) { handlebars.registerHelper({ rawText: function (data) { // Not a string? Try to recover and show a warning. if (data && typeof data !== 'string') { const content = typeof data._ === 'string' ? data._.trim() : data._ console.warn('There is an error in your data:') console.error(` "${content}"`) console.warn('Remove any XML tag, like
, , etc.') return data._ } return data }, prettyURL: function (link) { try { const url = new URL(link) const p = url.pathname === '/' ? '' : url.pathname return url.hostname.replace('www.', '') + p } catch (e) { return link } }, domainURL: function (link) { try { const url = new URL(link) return url.hostname.replace('www.', '') } catch (e) { return link } }, permalink: function (link) { // Do nothing if the `--pretty-links` flag is not activated. if (!config.commands.build.prettyLinks) return link if (link.endsWith('index.html')) { return path.dirname(link) + '/' } return link }, basename: function (name) { const ext = path.extname(name) return path.basename(name, ext) }, trim: function (data) { return (data + '').trim() }, // If a thumbnail exists, return it. // Otherwise, return the base image. thumbOrImage: function (image) { const imageName = path.join('images', image) const thumbName = path.join('images', `${image}.thumb.jpg`) const thumbPath = path.join(destination, thumbName) if (fs.existsSync(thumbPath)) return thumbName return imageName } }) } // Is the file an HTML file? function isValidPartial (file) { const ext = path.extname(file) return ext === '.html' } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { createTemplate } ================================================ FILE: lib/helpers/color-console.js ================================================ 'use strict' const chalk = require('chalk') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- function colorize (colorizer, args) { return Array.from(args).map(el => colorizer(el)) } function error () { console.error.apply(null, colorize(chalk.red, arguments)) } function warn () { console.warn.apply(null, colorize(chalk.yellow, arguments)) } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { error, warn, log: console.log } ================================================ FILE: lib/helpers/sfs.js ================================================ 'use strict' const fs = require('fs') const fse = require('fs-extra') const path = require('upath') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- // Create a directory if it doesn't exist yet. function createDir (dir) { if (fs.existsSync(dir)) { return false } fs.mkdirSync(dir, { recursive: true }) return true } function copyDirContent (source, target) { if (!fs.existsSync(source)) return // Create target dir if necessary. createDir(target) // Copy all files from source to target. findAllFiles(source).forEach((name) => { const nameWithoutSource = path.relative(source, name) const targetFile = path.join(target, nameWithoutSource) fse.outputFileSync(targetFile, fs.readFileSync(name)) }) } // Find all files in a directory structure. function findAllFiles (baseDir, { ignoredFolders = [], maxDepth = undefined } = {}) { const list = [] function search (dir, depth) { let entries try { entries = fs.readdirSync(dir) } catch (e) { return } entries.forEach(file => { file = path.join(dir, file) // File or directory? Ok. // Otherwise, discard the file (may have been deleted between readdir and stat). let stat try { stat = fs.statSync(file) } catch (e) { return } if (stat.isFile()) { list.push(file) } else if (stat.isDirectory()) { // The directory should be ignored? if (ignoredFolders.includes(path.basename(file))) { return } // Should we stop at this depth? if (maxDepth && (depth >= maxDepth)) { return } search(file, depth + 1) } }) } search(baseDir, 0) return list } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { createDir, copyDirContent, findAllFiles } ================================================ FILE: lib/helpers/sfs.test.js ================================================ 'use strict' const mock = require('mock-fs') const fs = require('fs') const sfs = require('./sfs') // ------------------------------------------------------------- // Data. // ------------------------------------------------------------- const fakeFileSystem = { product1: { 'data.json': '{ "type": "product", "title": "Fake Product" }', images: { 'img01.png': Buffer.from([]), 'img02.png': Buffer.from([]), 'img03.png': Buffer.from([]) }, emptyDir1: {} }, product2: { 'data.xml': '' }, misc: { product3: { 'data.md': 'Fake Product' }, emptyDir2: {} }, emptyDir3: {}, 'data.json': '{ "type": "company", "title": "Fake Company" }' } // ------------------------------------------------------------- // Setup. // ------------------------------------------------------------- beforeEach(() => { mock(fakeFileSystem) }) afterEach(() => { mock.restore() }) // ------------------------------------------------------------- // Tests. // ------------------------------------------------------------- describe('createDir()', () => { it('should create a directory if it does not exist', () => { // No dir: error. expect(() => fs.readdirSync('tmpdir')).toThrow() const result = sfs.createDir('tmpdir') expect(result).toBeTruthy() expect(fs.readdirSync('tmpdir').length).toBe([].length) }) it('should not create the directory if it already exists', () => { const result = sfs.createDir('product1') expect(result).toBeFalsy() }) }) describe('copyDirContent()', () => { it('should copy all the files of a folder to another', () => { const numberOfFiles = fs.readdirSync('product1/images').length sfs.copyDirContent('product1/images', 'copiedImages') expect(fs.readdirSync('copiedImages').length).toBe(numberOfFiles) }) }) describe('findAllFiles()', () => { it('should return an array containing every files on the FS', () => { const files = sfs.findAllFiles('.') // Number of files in mockfs. expect(files.length).toBe(7) }) it('should limit its search to one subfolder only', () => { const files = sfs.findAllFiles('.', { maxDepth: 1 }) expect(files.length).toBe(3) }) it('should ignore some folders', () => { const files = sfs.findAllFiles('.', { ignoredFolders: ['product1', 'product2'] }) expect(files.length).toBe(2) }) }) ================================================ FILE: lib/helpers/watcher.js ================================================ 'use strict' const path = require('upath') const chokidar = require('chokidar') const bs = require('browser-sync').create() const config = require('../config') const { assets } = require('../assets') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- // Simple watcher used by the end-user. // It only checks the data files and will launch a server on the build // folder. // It ignores the templates and CSS files changes. function installWatcher (startingFolder, callback) { bs.init({ server: config.commands.build.output, port: config.commands.build.port, ui: false, open: false, logLevel: 'silent' }) const watcher = createWatcher(path.join(startingFolder, '**/data.xml')) watcher.on('change', () => { callback() bs.reload() }) } // Complex watcher to develop the CSS and templates of this project. // Will check the data files, the templates AND the CSS. function installDevelopmentWatcher (startingFolder, callback) { const buildFolder = config.commands.build.output // BrowserSync will watch for changes in the assets CSS. // It will also create a server with the build folder and the assets. bs.init({ server: [buildFolder, assets], port: config.commands.build.port, files: path.join(assets, '**/*.css'), ui: false, open: false }) // Meanwhile, the watcher will monitor changes in the templates // and the data files. // Then, when a change occurs, it will regenerate the site through // the provide callback. const templateFolder = path.join(assets, '**/*.html') const dataFolder = path.join(startingFolder, '**/data.xml') const watcher = createWatcher(templateFolder, dataFolder) watcher.on('change', () => { callback() bs.reload() }) } function createWatcher (...folders) { const watcher = chokidar.watch(folders, { ignored: /(^|[/\\])\../, persistent: true }) return watcher } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { installWatcher, installDevelopmentWatcher } ================================================ FILE: lib/helpers/zip.js ================================================ 'use strict' const fs = require('fs') const path = require('upath') const archiver = require('archiver') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- function zip (name, destination, source, files) { if (files && files.length > 0) { const filename = path.join(destination, name) const output = fs.createWriteStream(filename) const archive = archiver('zip', { store: true }) archive.on('error', (err) => { throw err }) archive.pipe(output) files.forEach((f) => { archive.append(fs.createReadStream(path.join(source, f)), { name: f }) }) archive.finalize() return filename } } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = zip ================================================ FILE: lib/index.js ================================================ 'use strict' const fs = require('fs') const path = require('upath') const chalk = require('chalk') const console = require('./helpers/color-console') const generator = require('./core/generator') const config = require('./config') const assets = require('./assets') const sfs = require('./helpers/sfs') // ------------------------------------------------------------- // Module. // ------------------------------------------------------------- function runBuildCommand (launchOptions) { config.commands.build = launchOptions parseEntryPoint(launchOptions.entryPoint, (err, entry) => { if (err) { console.warn('No valid entry point provided. Use current directory instead') console.log('') generator.generate(process.cwd()) return } generator.generate(entry) }) } function parseEntryPoint (entry, cb) { // No file provided? Just return the current working dir. if (!entry) { return cb(null, process.cwd()) } // Check if the entry is valid. // And ensure that we use a directory. fs.stat(entry, (err, stat) => { if (err) { return cb(new Error('Not a file')) } if (stat.isDirectory()) { cb(null, entry) } else if (stat.isFile()) { cb(null, path.dirname(entry)) } else { cb(new Error('Not a directory or file')) } }) } function runNewCommand (type, destination) { if (type !== 'company' && type !== 'product') { console.error('Unknow type. Expected "company" or "product"') return } const isProduct = (type === 'product') const source = (isProduct ? assets.productTemplate : assets.companyTemplate) let targetDir = destination // Create subfolder for product. if (isProduct) { targetDir = path.join(destination, 'product') sfs.createDir(targetDir) } const target = path.join(targetDir, 'data.xml') // Copy files from examples to target directory. fs.writeFileSync(target, fs.readFileSync(source)) // Create images folder. sfs.createDir(path.join(targetDir, 'images')) console.log(`Creating new ${type} template: ${chalk.blue(target)}`) console.log('') console.log('Done! 👌') } // ------------------------------------------------------------- // Exports. // ------------------------------------------------------------- module.exports = { runBuildCommand, runNewCommand } ================================================ FILE: package.json ================================================ { "name": "presskit", "version": "0.13.0", "description": "Re-implementation of presskit() as a static site generator", "license": "MIT", "repository": "pixelnest/presskit.html", "author": { "name": "Pixelnest Studio", "url": "https://pixelnest.io/" }, "keywords": [ "presskit", "static site generator" ], "homepage": "https://github.com/pixelnest/presskit.html#readme", "bugs": "https://github.com/pixelnest/presskit.html/issues", "main": "lib/index.js", "bin": { "presskit": "bin/presskit" }, "scripts": { "start": "node bin/presskit build --watch --dev data", "build": "node bin/presskit build data", "test": "standard | snazzy && jest", "tester": "jest --watch", "docs": "./documentation", "docs:example": "node bin/presskit build -DLM -o docs/example data" }, "dependencies": { "archiver": "^7.0.1", "browser-sync": "^3.0.4", "camelcase": "^6.3.0", "chalk": "^4.1.2", "chokidar": "^3.6.0", "commander": "^12.1.0", "fs-extra": "^11.2.0", "handlebars": "^4.7.8", "sharp": "^0.33.0", "upath": "^2.0.1", "xml2js": "^0.6.2" }, "devDependencies": { "docco": "https://github.com/solarsailer/docco.git#ea3aea536562aed574d63904e2c2ab0138f4b40a", "jest": "^29.0.0", "mock-fs": "^5.2.0", "snazzy": "^8.0.0", "standard": "^17.1.0" }, "standard": { "globals": [ "beforeAll", "afterAll", "beforeEach", "afterEach", "it", "describe", "expect" ] }, "jest": { "testEnvironment": "node" } }