Skip to content

Instantly share code, notes, and snippets.

@Fedia
Last active January 30, 2017 23:59
Show Gist options
  • Save Fedia/d48bde020c15c4e5f1cf497d2dc1fcca to your computer and use it in GitHub Desktop.
Save Fedia/d48bde020c15c4e5f1cf497d2dc1fcca to your computer and use it in GitHub Desktop.
πŸŒ€ Eddy: Static Websites For The Rest of Us

Eddy: Static Websites For The Rest of Us

Static websites are awesome. No need to explain that in 2017. There are plenty of static site generators for Markdown-loving developers and Wordpress for all the rest. It is hard to manage a Medium-like blog or a single page website without setting up a server, learning Markdown or going for a proprietary website platform. Eddy to the rescue! You will need a few bucks a year and a Google account. No command line, no servers.

Modern browsers are great for both authoring content and website generation. Even on mobile. Eddy does just that - it is a serverless static content editor which works in Chrome, Firefox and Edge. For now Eddy can publish static HTML pages and images to Google Cloud Storage only. It is cheaper than Amazon S3 and has a built-in CDN. MIT.

eddy loqual

Setup Instructions

Step 1. Configure a Cloud Storage bucket for your website

Please follow this great tutorial from Google on how to set up a website on Cloud Storage. It assumes you have your own domain name. Then make all files public by default in default object permissions dialog by adding:

Entity Name Access
User allUsers Reader

Step 2. Enable Cloud Storage JSON API

Visit API Manager Library in Cloud Console and turn on Cloud Storage JSON API for your Google Cloud project.

Step 3. Create OAuth client ID

In Credentials tab of API Manager create a new OAuth cliend ID for a web application. Add http://your.domain.name to Authorized JavaScript origins. Authorized redirect URIs should be left blank. Please note the generated client ID which looks like 428088691083-31m6a38rvk2en63viu6vgv7eqj0n42bf.apps.googleusercontent.com.

Step 4. Upload a template page

Copy the example HTML to a plain text editor (Notepad). Put your own client ID near the bottom of the file like this:

<script>
_ed = { client_id: '428088691083-31m6a38rvk2en63viu6vgv7eqj0n42bf.apps.googleusercontent.com' }

Save the file as index.html and upload it to the website bucket.

Step 5. Done! Check it out

Visit http://your.domain.name/?edit to sign in with Google account and start editing.

//- polyfills
'use strict';
(function (p) {
if (!p.matches) p.matches = p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector;
})(Element.prototype);
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function (name, p) {
p = p || {};
var e = document.createEvent('CustomEvent');
e.initCustomEvent(name, p.bubbles || false, p.cancelable || false, p.detail);
return e;
};
window.CustomEvent.prototype = window.Event.prototype;
}
//- Eddy The Editor by //github.com/Fedia
// License: MIT
(function (ui_html) {
var api = window._ed || {};
if (!api.client_id) {
console.error('client_id is missing');
return;
}
if (!/^\?v\d+/.test(location.search)) {
refresh();
return;
}
document.body.insertAdjacentHTML('beforeend', ui_html);
if (api.plugins) {
api.plugins.forEach(load);
}
load('//apis.google.com/js/client:platform.js', init);
function load(url, cb) {
var s = document.createElement('script');
s.src = url;
s.onload = typeof cb === 'function' ? cb : null;
document.head.appendChild(s);
}
function init() {
auth(function () {
gapi.client.load('storage').then(editPage);
document.querySelector('#ed-gauth').style.display = 'none';
});
}
function auth(done) {
gapi.load('auth2', function () {
gapi.auth2.init({
client_id: api.client_id
}).then(function (auth) {
if (auth.isSignedIn.get()) {
done();
} else {
gapi.signin2.render('ed-gauth', {
'scope': 'https://www.googleapis.com/auth/devstorage.read_write',
'width': 220,
'height': 48,
'longtitle': true,
'theme': 'dark',
'onsuccess': done
});
}
});
});
}
function refresh() {
location.search = '?v' + Date.now();
}
function getContainer(root) {
if (!api.selector) {
api.selector = '.ed-container';
}
return (root || document).querySelector(api.selector);
}
function Eddy(el, opts) {
this.el = el;
var conf = this.conf = {
schema: el.tagName + ' > p, p > br, a, b, i',
paragraph: 'p'
};
if (opts) for (var k in opts) conf[k] = opts[k];
var each = Function.prototype.call.bind(Array.prototype.forEach);
var sanitize = this.applySchema.bind(this);
this.onMutation(el, function (mutations) {
if (el.isContentEditable) {
each(mutations, function (m) {
each(m.addedNodes, sanitize);
});
el.dispatchEvent(new CustomEvent('change', { bubbles: true }));
}
});
el.addEventListener('focus', function () {
setTimeout(function () {
// walkaround for Chrome warnings
var doc = el.ownerDocument;
doc.execCommand('enableObjectResizing', false, false);
doc.execCommand('defaultParagraphSeparator', false, conf.paragraph);
}, 1);
});
}
Eddy.prototype.onMutation = function (el, cb) {
var o = new MutationObserver(cb);
o.observe(el, { subtree: true, childList: true, characterData: true });
return o;
};
Eddy.prototype.applySchema = function (el) {
if (el.nodeType === el.ELEMENT_NODE && el.parentNode) {
var schema = this.conf.schema;
var doc = el.ownerDocument;
var iter = doc.createNodeIterator(el, NodeFilter.SHOW_ELEMENT, function (n) {
return n.matches(schema) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
}, false);
var inv, parent, next;
while (inv = iter.nextNode()) {
parent = inv.parentNode;
next = inv.nextSibling;
while (inv.firstChild) {
parent.insertBefore(inv.firstChild, next);
}
parent.removeChild(inv);
}
}
};
function editPage() {
var toolbar = document.querySelector('.ed-panel');
toolbar.style.display = '';
var editable = getContainer();
api.editor = new Eddy(editable);
editable.addEventListener('change', function () {
clearAttribute(editable, 'style');
clearAttribute(editable, 'class');
clearAttribute(editable, 'id');
if (editable.children.length === 0) {
document.execCommand('formatBlock', false, 'p');
}
});
editable.addEventListener('click', function (e) {
var el = e.target;
if (editable.isContentEditable && el.matches('a[href]')) {
e.preventDefault();
var url = prompt('Link', el.getAttribute('href'));
if (url !== null) {
el.setAttribute('href', url);
}
}
});
editable.dispatchEvent(new CustomEvent('beforeedit', { bubbles: true }));
editable.contentEditable = true;
editable.dispatchEvent(new CustomEvent('edit', { bubbles: true }));
}
function clearAttribute(root, attr) {
var nodes = root.querySelectorAll('[' + attr + ']');
for (var i = 0; i < nodes.length; i++) {
nodes[i].removeAttribute(attr);
}
}
function getBucketName() {
return location.hostname;
}
function getObjectName() {
return location.pathname.replace(/^\/|\/$/g, '') || 'index.html';
}
function getPageHTML(name, cb) {
return gapi.client.storage.objects.get({
bucket: getBucketName(),
object: name || getObjectName(),
alt: 'media'
}).then(function (res) {
if (cb) cb(res.body);
return res.body;
}, function (e) {
alert(e.result.error.message);
});
}
api.uploadFile = uploadFile;
function uploadFile(name, data, headers) {
return gapi.client.request({
path: '/upload/storage/v1/b/' + getBucketName() + '/o',
method: 'POST',
params: {
name: name,
uploadType: 'media'
},
headers: headers || {},
body: data
});
}
api.deleteFile = deleteFile;
function deleteFile(name) {
return gapi.client.storage.objects['delete']({
bucket: getBucketName(),
object: name
}).then(function (res) {
return res;
}, function (e) {
alert(e.result.error.message);
});
}
function parseHTML(html) {
// return (new DOMParser()).parseFromString(html, 'text/html');
var dom = document.implementation.createHTMLDocument();
dom.documentElement.innerHTML = html;
return dom;
}
function toHTML(dom) {
var garbage = ' xmlns="http://www.w3.org/1999/xhtml"';
return new XMLSerializer().serializeToString(dom).replace(garbage, '');
}
api.save = savePage;
function savePage() {
var saveAs = '';
while (!saveAs.length || /[^\w\d\.\-]/.test(saveAs)) {
saveAs = window.prompt('URL', saveAs || getObjectName());
if (saveAs === null) return;
}
if (saveAs.substr(-5) !== '.html') {
saveAs += '.html';
}
var container = getContainer(); //.cloneNode(true);
container.dispatchEvent(new CustomEvent('beforesave', { bubbles: true }));
var template = api.template || null;
return getPageHTML(template).then(function (html) {
var doc = parseHTML(html);
getContainer(doc).innerHTML = container.innerHTML;
container.dispatchEvent(new CustomEvent('save', {
bubbles: true,
detail: { document: doc }
}));
return toHTML(doc);
}).then(function (html) {
return uploadFile(saveAs, html, {
'Content-Type': 'text/html'
});
}).then(function () {
location.href = '/' + saveAs;
});
}
if ('index.html' === getObjectName()) {
//document.querySelector('.ed-btn-deletepage').style.display = 'none';
document.querySelector('.ed-actions .ed-btn:last-of-type').style.display = 'none';
}
api['delete'] = deletePage;
function deletePage() {
var page = getObjectName();
if (page === 'index.html') {
return;
}
if (confirm('Delete ' + page + '?')) {
return deleteFile(page);
}
}
document.addEventListener('save', function (e) {
var doc = e.detail.document;
var page_title = getContainer(doc).querySelector('h1,h2');
if (page_title) {
var title = doc.querySelector('title');
title.textContent = page_title.textContent;
}
});
api.cmd_format = function (cmd) {
document.execCommand(cmd, false, null);
};
document.addEventListener('beforeedit', function () {
api.editor.conf.schema += ', ' + api.selector + '> h1, ' + api.selector + '> h2';
});
var cmd_block_edgevalues = {};
api.cmd_block = function (tag) {
var cmd = 'formatBlock';
var val = document.queryCommandValue(cmd);
if (val === tag || val === cmd_block_edgevalues[tag]) {
tag = 'p';
}
document.execCommand(cmd, false, tag);
cmd_block_edgevalues[tag] = document.queryCommandValue(cmd);
};
api.cmd_clear = function () {
document.execCommand('unlink', false, null);
document.execCommand('removeFormat', false, null);
};
api.cmd_link = function (val) {
var url = prompt('Link', val || 'https://');
if (url) {
document.execCommand('createLink', false, url);
}
};
document.addEventListener('click', function (e) {
var cl = 'ed-menu__show';
var sel = '.' + cl;
if (!e.target.matches(sel)) {
var btn = document.querySelector(sel);
if (btn) btn.classList.remove(cl);
}
});
api.menu = function (btn) {
btn.classList.add('ed-menu__show');
};
})('<style>@import "//gistcdn.githack.com/Fedia/d48bde020c15c4e5f1cf497d2dc1fcca/raw/styles.css"</style>\n<div class="ed-panel" style="display:none">\n <span>πŸŒ€</span>\n <span class="ed-tools">\n <button class="ed-btn" onclick="_ed.cmd_block(\'h1\')"><b>H</b></button>\n <button class="ed-btn" onclick="_ed.cmd_block(\'h2\')"><b><small>H</small></b></button>\n <button class="ed-btn" onclick="_ed.cmd_format(\'bold\')"><b><small>b</small></b></button>\n <button class="ed-btn" onclick="_ed.cmd_format(\'italic\')"><i><small>i</small></i></button>\n <button class="ed-btn" onclick="_ed.cmd_clear()"><i><b>T</b></i><small>x</small></button>\n <button class="ed-btn" onclick="_ed.menu(this)">+</button>\n <div class="ed-menu">\n <button class="ed-btn" onclick="_ed.cmd_link()">πŸ”— Link</button>\n </div>\n </span>\n <span class="ed-actions">\n <button class="ed-btn" onclick="_ed.save()">πŸ’Ύ</button>\n <button class="ed-btn" onclick="_ed.menu(this)">β ‡</button>\n <div class="ed-menu">\n <button class="ed-btn ed-btn-deletepage" onclick="_ed.delete()">πŸ—‘οΈ&nbsp;Delete</button>\n </div>\n </span>\n</div>\n<div id="ed-gauth"></div>');
//- Image upload plugin
(function (api) {
var btn_html = '<button class="ed-btn" onclick="_ed.cmd_img()">πŸ“· Picture</button>';
var placeholder_img = 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB2aWV3Qm94PScwIDAge3t3fX0ge3tofX0nPjxkZWZzPjxzeW1ib2wgaWQ9J2EnIHZpZXdCb3g9JzAgMCA5MCA2Nicgb3BhY2l0eT0nMC4zJz48cGF0aCBkPSdNODUgNXY1Nkg1VjVoODBtNS01SDB2NjZoOTBWMHonLz48Y2lyY2xlIGN4PScxOCcgY3k9JzIwJyByPSc2Jy8+PHBhdGggZD0nTTU2IDE0TDM3IDM5bC04LTYtMTcgMjNoNjd6Jy8+PC9zeW1ib2w+PC9kZWZzPjx1c2UgeGxpbms6aHJlZj0nI2EnIHdpZHRoPScyMCUnIHg9JzQwJScvPjwvc3ZnPg==';
document.addEventListener('beforeedit', function (e) {
init(e.target);
});
function init(editable) {
document.querySelector('.ed-tools .ed-menu').insertAdjacentHTML('beforeend', btn_html);
api.editor.conf.schema += ', p > img';
editable.addEventListener('click', function (e) {
var el = e.target;
if (editable.isContentEditable && el.matches('img')) {
e.preventDefault();
img_upload(el);
}
});
}
api.cmd_img = function () {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
var img = new Image();
img.src = placeholder_img;
range.insertNode(img);
}
};
function readFile(file, cb) {
var fr = new FileReader();
fr.onloadend = function (e) {
var uri = fr.result;
cb(null, uri.substr(uri.indexOf(',') + 1), fr);
};
fr.onerror = function (e) {
cb(e, null);
};
fr.readAsDataURL(file);
}
function img_upload(dest) {
var input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.onchange = function () {
var f = input.files[0];
if (!f || f.type.substr(0, 6) !== 'image/') return;
var path = 'img/' + f.name;
readFile(f, function (err, data, reader) {
if (err) return alert(err);
dest.src = reader.result;
api.uploadFile(path, data, {
'Content-Type': f.type,
'Content-Encoding': 'base64'
}).then(function () {
dest.src = '/' + path + '?v' + Date.now();
});
});
};
input.click();
}
})(_ed);
.ed-panel {
position: fixed;
top: -1px;
left: -1px;
right: -1px;
z-index: 99;
background: #eee;
border: 1px solid #cfcfcf;
height: 32px;
padding: 4px;
font: 26px sans-serif;
line-height: 1em;
color: #444;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
@media (min-width: 481px) {
.ed-panel {
font-size: 18px;
}
}
html {
margin-top: 32px;
}
.ed-btn {
border: 0;
font: inherit;
background: transparent;
color: inherit;
outline: none;
cursor: pointer;
position: relative;
border-radius: 3px;
padding: 0;
padding: 2px;
margin: 0 2px;
height: 1em;
line-height: 1em;
/* min-height: 20px; */
display: inline-block;
text-align: inherit;
}
.ed-btn:active {
background: rgba(0, 0, 0, 0.1);
}
.ed-btn:after {
content: "";
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background-image: radial-gradient(circle, #000 10%, transparent 10%);
background-repeat: no-repeat;
background-position: 50%;
transform: scale(10, 10);
opacity: 0;
transition: transform .5s, opacity 1s;
}
.ed-btn:active:after {
transform: scale(0, 0);
opacity: .2;
transition: 0s;
}
.ed-tools,
.ed-actions {
position: relative;
}
.ed-tools > .ed-btn {
max-width: 1.2em;
}
.ed-menu {
position: absolute;
right: 4px;
top: 4px;
padding: 4px;
background: #eee;
border: 1px solid #cfcfcf;
display: flex;
flex-direction: column;
align-items: left;
justify-content: left;
text-align: left;
transform: scale(0, 0);
transform-origin: right top;
transition: transform .3s;
}
.ed-menu__show + .ed-menu {
transform: scale(1, 1);
}
.ed-menu .ed-btn {
margin: 8px 4px;
}
#ed-gauth {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 99;
overflow: hidden;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
}
#ed-gauth:empty:after {
content: "πŸ’πŸ’¨";
display: block;
font: 20px sans-serif;
color: #666;
white-space: nowrap;
}
[contenteditable] {
outline: none;
}
<!DOCTYPE html>
<html><head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Eddy Template</title>
<style>
body {
background: #fff;
color: #444;
margin: 0;
}
.content {
font: 16px Georgia, serif;
line-height: 1.5;
padding: 2em 11% 3em 11%;
}
@media only screen and (min-width: 769px) {
.content {
padding: 2em 21.6447% 3em 21.6447%;
}
}
@media only screen and (min-width: 481px) {
.content {
font-size: 1.6em;
}
}
.content p {
margin: .9em 0 0 0;
}
.content h1,
.content h2 {
font-family: sans-serif;
margin: .67em 0;
line-height: 1.25;
}
.content img {
width: 100%;
max-width: 900px;
margin: .9em auto;
}
</style>
</head>
<body>
<article class="content ed-container">
<h1>Hello world</h1>
<p>You are so beautiful.</p>
</article>
<script>
_ed = { client_id: '<your client id>' }
;(function(t,a,c,k){
c=sessionStorage[t]=location.search=='?edit'?1:sessionStorage[t]||0
if(c==1)k=document.createElement('script'),k.src=a,document.head.appendChild(k)
})('_ed_edit','https://gistcdn.githack.com/Fedia/d48bde020c15c4e5f1cf497d2dc1fcca/raw/index.js')
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment