...
 
Commits (62)
* + * {
margin-top: 1.5em;
}
body {
background: #17191F;
color: #f7f7f7;
font-family: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, 'helvetica neue', helvetica, ubuntu, roboto, noto, 'segoe ui', arial, sans-serif;
margin-top: 1rem;
/*margin: 0 4rem;*/
/*display: flex;*/
/*flex-direction: column;*/
/*align-items: center;*/
}
#container {
background: #1F232B;
display: flex;
flex-direction: column;
/* align-items: center;*/
width: 50%;
margin: auto;
border-radius: .4rem;
padding: 1em;
}
a {
text-decoration: none;
color: #2b90d9;
}
a:hover, a:active {
text-decoration: underline;
}
img {
width: 20%;
margin: 1.5rem 0;
}
h1 {
font-size: 1.8rem;
text-align: center;
margin: 0
}
form {
}
label {
font-size: 1.1rem;
display: block;
margin-bottom: 0.6rem;
}
input[type='text'],
input[type='email'],
input[type='date'] {
-webkit-appearance: none;
-moz-appearance: none;
border-style: solid;
border-width: 1px;
border-color: rgba( 0, 0, 0, .2 );
padding: .5rem 0;
margin-bottom: 1rem;
display: block;
width: 100%;
max-width: 100%;
min-width: 100%;
}
textarea {
-webkit-appearance: none;
-moz-appearance: none;
border-style: solid;
border-width: 1px;
border-color: rgba( 0, 0, 0, .2 );
padding: .5rem 0;
margin-bottom: 1rem;
display: block;
width: 100%;
min-width: 100%
max-width: 100%;
}
.checklist li {
list-style-type: none;
margin: initial;
}
#submit {
/*display: inline;*/
/*float: left;*/
background: #56A7E1;
font-weight: bold;
padding: 1rem;
margin-top: 2rem;
margin-bottom: 2rem;
text-align: center;
font-size: 1rem;
border-style: none;
border-width: 0;
color: white;
border-radius: .25rem;
/*width: 100%;*/
}
#submit:hover {
cursor: pointer;
}
@media (max-width: 1000px) {
#container {
width: 90%
}
img {
width: 30%;
}
h1 {
font-size: 1.5rem;
}
label {
font-size: 1rem;
}
form {
width: 75%;
}
}
/* Really, the following styles are just slapdash scaffolding */
#top {
display: table;
width: 100%;
}
#top > * {
display: table-cell;
vertical-align: middle;
}
#description {
font-size: xx-large;
text-align: center;
}
#menu ul, #menu li {
display: inline-block;
}
#menu li {
background: #1F232B;
border-radius: .4rem;
padding: 0.5em;
margin: 1em 0.1em;
}
#left-sidebar {
}
module.exports = {
theme: {
timezone: 'Europe/London',
title: 'Example Title',
description: 'Example description',
logo: 'images/example_logo.svg',
urls: {
// The site home URL - logo links to this
home: 'https://example.com',
// The top level url of the wiki
base: 'https://example.com/wiki',
// An url to prepend author usernames to
authorbase: 'https://git.coop/',
},
},
port: 9999,
email: {
server: {
......@@ -10,12 +24,25 @@ module.exports = {
tls: true,
},
registration: {
// REQUIRED field!
// REQUIRED fields!
// Where replies to notifications/acknowledgements go
from: "Registrations <registrations@example.com>",
// REQUIRED field!
// Where notifications go (acknowledgements go to the
// registrant).
to: "Registrations <registrations@example.com>",
subject: "Someone has registered!",
}
// The subject line for notifications
notification: "Someone has registered!",
// The subject line for acknowledgements
acknowledgement: "Thanks for registering!",
},
templates: {
// Note: recommended place for templates is in src/email-templates/.
// notification: 'src/email-templates/notification-template.hbs',
// acknowledgement: 'src/email-templates/acknowledgement-template.hbs',
// Theme config file included in template context under 'config' key.
// Also, request under 'request'
},
},
webhook: {
path: '/example-path',
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="description" content="{{ description }}">
<link rel="stylesheet" href="/style.css" media="all">
<title>{{ title }}</title>
</head>
<body>
<div id="top">
<img src="/img/social_coop_logo.svg" alt="social.coop logo: three white trees in a green circle">
<span id="menu">{{> menu }}</span>
</div>
<div>
<span id="left-sidebar">
{{> posts }}
</span>
<span id="container">
<div id="description">{{ title }}</div>
<div id="content">
{{{ contents }}}
</div>
</span>
</div>
<div id="bottom">
<span>
Last modified: <a href="{{{ wikiLink }}}">{{ git.authored }}</a>
</span>
<span>
By: <a ref="{{{ authorLink }}}">{{ git.author }}</a>
</span>
</div>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{{ title }}</title>
<meta name="description" content="{{ description }}">
</head>
<body>
<header>
<p>
<a href="/">Home</a>
</p>
</header>
<h1>{{ title }}</h1>
<time>{{ date }}</time>
{{{ contents }}}
<footer>
<p>Generated with {{ generator }} &mdash; <a href="{{ url }}">{{ url }}</a></p>
</footer>
</body>
</html>
'use strict';
/** Defines functions which map registration data into HTML email
* content for user acknowledgements and admin notifications.
*/
const handlebars = require('handlebars');
const fs = require('fs');
const handlebarsHelpers = require('./helpers.js');
const config = require('./config.js');
const maillog = require('./logging.js').loggers.mail;
// Register the helpers - we use the same ones as supplied to the
// theme, out of laziness.
for(var name in handlebarsHelpers) {
handlebars.registerHelper(name, handlebarsHelpers[name]);
}
function defineTemplateLoader(file, name, defaultTemplate) {
if (!file) {
maillog.warn(`no configured ${name} template file, using default`);
return defaultTemplate;
}
maillog.info(`${name} will be loaded from the file: ${file}`);
return (context) => {
if (fs.existsSync(file)) {
maillog.info(`loading ${name} from file: ${file}`);
const content = fs.readFileSync(file, 'utf8');
return handlebars.compile(content)(context);
}
else {
maillog.warn(`using default as no ${name} found at ${file}`);
return defaultTemplate(context);
}
};
}
// Define functions which returns the email templates. These are
// re-loaded on each send so that they can be defined by files in the
// source (with inline handlebars markup). (These templates can be
// excluded from appearing in the output wiki by various means.) The
// one required field in submitted data is an 'email' field with a
// recipient address. All fields, plus parts of the config, are
// supplied to the templates.
var notificationTemplate, acknowledgementTemplate;
if (config.email && config.email.templates) {
notificationTemplate = config.email.templates.notification;
acknowledgementTemplate = config.email.templates.acknowledgement;
}
module.exports = {
notification: defineTemplateLoader(
notificationTemplate,
'email notification template',
(context) =>
`<pre>${JSON.stringify(context.request.body, null, 2)}</pre>`
),
acknowledgement: defineTemplateLoader(
acknowledgementTemplate,
'email acknowledgement template',
(context) => 'Thanks! We shall be in touch.'
),
};
......@@ -4,7 +4,15 @@ const toText = require('html2plaintext');
module.exports = (serverConfig = {}) => {
async function send(config, html) {
/** Sends a notification email.
*
* @param config - the emailjs config
* @param html - the HTML message (will have a text version added)
* @param json - optional JSON attachment (the registration data)
* @returns a promise which can be used to obtain the success
* or failure of this (potentially slow) operation.
*/
async function send(config, html, json) {
if (!config.from)
throw new Error("must send with a from: parameter");
if (!config.to)
......@@ -12,12 +20,17 @@ module.exports = (serverConfig = {}) => {
var html = String(html);
const server = email.server.connect(serverConfig);
const attachments = [{data: html, alternative: true}];
if (json)
attachments.push({
data: JSON.stringify(json),
name: 'form-data.json',
type: 'application/json'
});
const message = {
...config,
text: toText(html),
attachment: [
{data: html, alternative: true},
],
attachment: attachments
};
// Send the message and await completion. Return
......
/**
Minimal/custom git functionality.
Basically, just pull, and gather working directory metadata.
*/
'use strict';
const shell = require('shelljs');
shell.config.silent = true;
shell.config.fatal = true;
module.exports = (opts = {}) => {
if (!opts.src)
opts.src = './src';
/** Helper function for accumulateFiles. Indicates when *not* to
* update the working value a when compare to a commit b
*/
const compare = {
first: (a,b) => a < b,
last: (a,b) => a > b,
};
// Reduce accumulator for git-log
//
// @param meta - the accumulator object, expected to be:
// { list: <list>, map: <map> }
// where <list> is an array in which to
// build an ordered list of commit IDs,
// and <map> is a simple object in which
// to map commit IDs to to author and time
// information in this form:
// { authored: <date>, author: <string>
// committed: <date>, committer: <string>}
// @param line - a line from git diff-tree to parse
//
// @returns an updated accumulator object
function accumulateCommits(meta, line) {
// fields are:
// commit ID,
......@@ -17,28 +46,64 @@ module.exports = (opts = {}) => {
var authored = fields[1] && new Date(fields[1]*1000);
var committed = fields[3] && new Date(fields[3]*1000);
meta[fields[0]] = {
meta.map[fields[0]] = {
authored: authored,
author: fields[2],
committed: committed,
committer: fields[4],
committer: fields[4],
};
meta.list.push(fields[0]);
return meta;
}
// Reduce accumulator for git-diff-tree
// Reduce accumulator for git-diff-tree.
//
// @param meta - the accumulator object:
// { commits: <commitlog>, files: <filemap> }
// where <commitlog> is the output from
// accumulateCommits, and <filemap> is an object
// in which to map file paths to information
// about the first and last commits
// touching it (omitting any commits prior to
// a delete):
// { first: <commit>, last: <commit> }
// Where <commit> is an entry in the <commitlog>,
// @param line - a line from git diff-tree to parse
//
// @returns the updated accumulator object with the complete file
// map
function accumulateFiles(meta, line) {
const match = line.match(/([A-Z])\s+(.*)/);
if (match) {
// Parse a file modification
if (match[1] === 'D') {
delete meta.files[match[2]];
delete meta.files[match[2]]; // forget this entry
}
else {
meta.files[match[2]] =
meta.commits[meta.commitid];
const fileinfo = meta.files[match[2]] ||
(meta.files[match[2]] = {first: {}, last: {}});
const commitinfo = meta.commits[meta.commitid];
// A common block of code which compares and updates a
// fileinfo user/date value when appropriate.
const update = (who, what, when) => {
if (fileinfo[when][what] &&
compare[when](fileinfo[when][what],
commitinfo[what]))
return; // nothing to do
// update the records
fileinfo[when][what] = commitinfo[what];
fileinfo[when][who] = commitinfo[who];
};
update('author', 'authored', 'first');
update('author', 'authored', 'last');
update('committer', 'committed', 'first');
update('committer', 'committed', 'last');
}
}
else {
......@@ -70,7 +135,59 @@ module.exports = (opts = {}) => {
};
}
/** gets git metadata */
/** Gets git metadata for all the files in the repo.
*
* Example return format:
*
* { '.gitignore':
* { first:
* { authored: 2019-01-28T21:46:14.000Z,
* author: 'bob',
* committed: 2019-01-28T21:46:14.000Z,
* committer: 'alice' },
* last:
* { authored: 2019-02-10T10:56:29.000Z,
* author: 'alice',
* committed: 2019-02-10T23:32:17.000Z,
* committer: 'bob' },
* span: { authored: 1084215000, committed: 1129563000 } },
* 'package.json':
* { first:
* { authored: 2019-01-28T21:49:24.000Z,
* author: 'alice',
* committed: 2019-01-28T23:31:58.000Z,
* committer: 'alice' },
* last:
* { authored: 2019-02-20T10:45:48.000Z,
* author: 'bob',
* committed: 2019-02-25T17:45:38.000Z,
* committer: 'alice' },
* span: { authored: 1947384000, committed: 2398420000 } },
* 'serve.js':
* { first:
* { authored: 2019-01-28T23:33:30.000Z,
* author: 'alice',
* committed: 2019-01-28T23:37:09.000Z,
* committer: 'bob' },
* last:
* { authored: 2019-02-10T10:54:55.000Z,
* author: 'alice',
* committed: 2019-02-10T23:32:17.000Z,
* committer: 'bob' },
* span: { authored: 1077685000, committed: 1122908000 } },
* // ...
* }
*
* Implementation adapted from:
*
* https://stackoverflow.com/questions/19166483/git-how-to-list-all-files-under-version-control-along-with-their-author-date
*
* Note: first and last dates are more strictly the min and max
* found since the last delete. authorship taken from equivalent
* commits.
*
* Note: does not currently detect renames.
*/
function fileInfoSync() {
const execOpts = {cwd: opts.src, silent: true};
......@@ -87,13 +204,11 @@ module.exports = (opts = {}) => {
// Parse the results into a map of commit IDs to timestamps
const commits = result1.stdout
.split(/\n/)
.reduce(accumulateCommits, {});
const commitids = Object.keys(commits);
.reduce(accumulateCommits, {map:{}, list: []});
// Get the files touched by each commit
const result2 = shell
.ShellString(commitids.join("\n"), {silent: true})
.ShellString(commits.list.join("\n")+"\n", {silent: true})
.exec('git diff-tree -r --root --name-status --stdin',
execOpts);
......@@ -106,8 +221,16 @@ module.exports = (opts = {}) => {
const acc = result2
.stdout
.split(/\n/)
.reduce(accumulateFiles, {commits, files:{}});
.reduce(accumulateFiles, {commits:commits.map, files:{}});
for(const file in acc.files) {
const info = acc.files[file];
info.span = {
authored: info.last.authored - info.first.authored,
committed: info.last.committed - info.first.committed,
};
}
return acc.files;
}
......
const Handlebars = require('handlebars');
const moment = require('moment-timezone');
function a(text, path, baseurl) {
const url = new URL(path, baseurl);
text = Handlebars.Utils.escapeExpression(text);
return `<a href="${url}">${text}</a>`;
}
function getopts(args) {
if (args.length > 0) {
args.length -= 1;
const opts = args[args.length];
delete args[args.length];
return Array.prototype.concat.apply([opts], args);
}
return [{}];
}
module.exports = {
wikiLink: function() {
const [opts, path] = getopts(arguments);
// This requires wikiurl to be defined in global metadata
path = path.replace(/[.]md$/, '');
path = path.replace(/^\//, '');
const wikiurl = opts.data.root.wikiurl.replace(/\/+$/,'/');
return new Handlebars.SafeString(wikiurl+path);
},
authorLink: function() {
const [opts, author] = getopts(arguments);
const link = a(author, author, opts.data.root.config.urls.authorbase);
return new Handlebars.SafeString(link);
},
/** Html escapes text, but converts adjacent newlines into paragraph tags */
parags: function() {
var [opts, text] = getopts(arguments);
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/\n\s*\n/mg, '<p>');
return new Handlebars.SafeString(text);
},
// Ghost emulations
asset: function(path) {
return '/'+path;
},
content: function() {
const [opts] = getopts(arguments);
return opts.data.root.contents;
},
'is-in': function() {
var [options, types] = getopts(arguments);
const collections = options.data.root.collection;
if (collections) {
switch(typeof types) {
case 'string':
types = types.split(',').map(s => s.trim());
break;
case 'array':
break;
case 'undefined':
throw new Error("#is helper must have a parameter");
default:
throw new Error("#is helper parameter must be comma-delimited string or array");
}
// search collections and types to find a matching type
const ismatch = types.reduce(
(result, type) => result || collections.indexOf(type) >= 0,
false
);
if (ismatch) {
return options.fn(this);
}
}
return options.inverse(this);
},
foreach: function(items, options) {
// we need options
if (!options) {
throw new Error('Must pass iterator to #foreach');
}
function filterItemsByVisibility(items, options) {
return items; // stub, reimplement later
}
if (options.data && options.ids) {
contextPath = Handlebars.Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (typeof(items) === 'function') {
items = items.call(this);
}
// Exclude items which should not be visible in the theme
items = filterItemsByVisibility(items, options);
// Initial values set based on parameters sent through. If
// nothing sent, set to defaults
var fn = options.fn,
inverse = options.inverse,
columns = options.hash.columns,
length = items.length,
limit = Math.floor(options.hash.limit) || length,
from = Math.floor(options.hash.from) || 1,
to = Math.floor(options.hash.to) || length,
output = '',
data,
contextPath;
// If a limit option was sent through (aka not equal to
// default (length)) and from plus limit is less than the
// length, set to to the from + limit
if ((limit < length) && ((from + limit) <= length)) {
to = (from - 1) + limit;
}
if (options.data) {
data = Handlebars.createFrame(options.data);
}
function execIteration(field, index, last) {
if (data) {
data.key = field;
data.index = index;
data.number = index + 1;
data.first = index === from - 1; // From uses 1-indexed, but array uses 0-indexed
data.last = !!last;
data.even = index % 2 === 1;
data.odd = !data.even;
data.rowStart = index % columns === 0;
data.rowEnd = index % columns === (columns - 1);
}
output = output + fn(items[field], {
data: data,
blockParams: Handlebars.Utils.blockParams([items[field], field], [contextPath + field, null])
});
}
function iterateCollection(context) {
// Context is all posts on the blog
var current = 1;
// For each post, if it is a post number that fits within
// the from and to, send the key to execIteration to be
// added to the page
context.forEach((item, key) => {
if (current < from) {
current += 1;
return;
}
if (current <= to) {
execIteration(key, current - 1, current === to);
}
current += 1;
});
}
if (items && typeof items === 'object') {
iterateCollection(items);
}
return output;
},
post: function() {
const [opts] = getopts(arguments);
if (opts.fn) {
const str = opts.fn(this, opts.data);
//console.log("inner", this, arguments, `(${str})`);
return str;
}
else {
return opts.post;
}
},
img_url: function() {
const [options, path] = getopts(arguments);
const baseurl = options.data.root.config.urls.base;
var outputUrl = new URL(path, baseurl).href;
if (!options.hash.absolute)
outputUrl = outputUrl.substring(baseurl.length);
outputUrl = encodeURI(outputUrl);
return new Handlebars.SafeString(outputUrl);
},
encode: function(string, options) {
var uri = string || options;
return new Handlebars.SafeString(encodeURIComponent(uri));
},
// meta_title
url: function() {
const [options, path] = getopts(arguments);
const baseurl = options.data.root.config.urls.base;
var outputUrl = new URL(path || this.path, baseurl);
// FIXME: is absolute /blah or https://foo/blah?
if (!options.hash.absolute)
outputUrl = outputUrl.href;
else
outputUrl = outputUrl.href.substring(outputUrl.origin.length);
outputUrl = encodeURI(outputUrl);
return new Handlebars.SafeString(outputUrl);
},
date: function() {
var [options, date] = getopts(arguments);
var timezone, format, timeago, timeNow, dateMoment;
const root = options.data.root;
if (!date) {
timezone = root.config.timezone;
// set to published_at by default, if it's available
// otherwise, this will print the current date
if (root.git) {
date = moment(root.git.authored).tz(timezone).format();
}
}
// ensure that context is undefined, not null, as that can cause errors
date = date === null ? undefined : date;
format = options.hash.format || 'MMM DD, YYYY';
timeago = options.hash.timeago;
timezone = this.timezone;
timeNow = moment().tz(timezone);
// i18n: Making dates, including month names, translatable to any language.
// Documentation: http://momentjs.com/docs/#/i18n/
// Locales: https://github.com/moment/moment/tree/develop/locale
dateMoment = moment(date);
//dateMoment.locale(i18n.locale()); // Commented to avoid i18n dep
if (timeago) {
date = timezone ? dateMoment.tz(timezone).from(timeNow) : dateMoment.fromNow();
} else {
date = timezone ? dateMoment.tz(timezone).format(format) : dateMoment.format(format);
}
return new Handlebars.SafeString(date);
},
};
......@@ -7,9 +7,10 @@ const markdown = require('metalsmith-markdown');
const permalinks = require('metalsmith-permalinks');
const metalsmithDebug = require('metalsmith-debug');
const discoverPartials = require('metalsmith-discover-partials');
const discoverHelpers = require('metalsmith-discover-helpers');
const singular = require('pluralize').singular;
const winston = require('winston');
const fs = require('fs');
const helpers = require('./lib/helpers.js');
const git = require('./lib/git.js')();
const debug = require('debug')('metalsmith.js');
const config = require('./lib/config.js');
......@@ -28,14 +29,14 @@ function forFiles(onFile) {
/** Iterates over the files, adding metadata which isn't already set */
function addMeta(meta) {
return forFiles((data, file) => {
return forFiles((data, file, metalsmith) => {
for(const key in meta) {
if (key in data)
continue;
var val = meta[key];
if (typeof(val) === 'function')
val = val(data, file);
val = val(data, file, metalsmith);
if (val !== undefined)
data[key] = val;
}
......@@ -43,13 +44,16 @@ function addMeta(meta) {
}
/** Converts a path to a title, so titles don't need to be defined */
function pathToTitle(path) {
return path
.split('/')
.pop()
.replace(/[.][^.]*/, '')
.replace(/^(.)/, (_, ch) => ch.toUpperCase())
.replace(/-(.?)/g, (_, ch) => ' '+ch.toUpperCase());
function pathToTitle(path, truncate = true) {
var path = path.split('/')
if (truncate && path.length)
path = path.splice(path.length-1);
return path.map(
c =>
c.replace(/[.](md|html)$/, '')
.replace(/^(.)/, (_, ch) => ch.toUpperCase())
.replace(/-(.?)/g, (_, ch) => ' '+ch.toUpperCase())
).join('/');
}
/** Extract a layout name if the file is part of a collection, and an
......@@ -58,12 +62,13 @@ function layoutFromCollection(data, opts = {}) {
if (!opts.extension)
opts.extension = '.hbs';
if (!opts.layoutsDir)
opts.layoutsDir = 'layouts';
opts.layoutsDir = 'theme';
if (data.collection instanceof Array &&
data.collection.length > 0) {
var name = singular(data.collection[0]);
// Get the first collection item, ignore the others.
var layout = `${data.collection[0]}${opts.extension}`;
var layout = `${name}${opts.extension}`;
// Only use it if there is a corresponding layout file
if (fs.existsSync(`${__dirname}/${opts.layoutsDir}/${layout}`))
......@@ -71,8 +76,42 @@ function layoutFromCollection(data, opts = {}) {
}
}
/** Plugin to add collection index pages */
function collectionIndexes(opts = {}) {
return function(files, metalsmith) {
const meta = metalsmith.metadata();
for(var indexfile in opts) {
var indexopts = opts[indexfile];
if (typeof(indexopts) !== 'object')
indexopts = {collection: indexopts};
const index = meta.collections[indexopts.collection];
if (!index)
continue;
const layout = indexopts.layout ||
`${indexopts.collection}-index`;
files[indexfile] = {
contents: Buffer.alloc(0),
layout: layout,
filename: indexfile,
path: indexfile,
};
}
};
}
const gitMeta = git.fileInfoSync();
// A function to compare posts by git.first.authored
function postCreation(a, b) {
a = gitMeta[a.path];
b = gitMeta[b.path];
a = a? a.first.authored : 0;
b = b? b.first.authored : 0;
return b - a;
}
// Dummy metadata to omit voluminous/circular crap fields
function elideMeta(meta) {
return {
......@@ -91,52 +130,69 @@ function elideMeta(meta) {
module.exports = () => {
return Metalsmith(__dirname)
.metadata({
// These defaults can be overridden in pages
sitename: "Social Coop",
siteurl: "https://social.coop/",
wikiurl: "https://git.coop/social.coop/general/wikis/",
description: "A cooperatively-run corner of the fediverse.",
generatorname: "Metalsmith",
generatorurl: "http://metalsmith.io/",
})
.ignore([
'**/*~', // Emacs backup files
'.*', // dotfiles
'email-templates', // email templates directory
])
.clean(true) // clean destination before building
.use(metalsmithDebug())
.use(collections({ // Group all blog posts by internally
.use(collections({
posts: {
pattern: 'posts/*.md', // adding key 'collections':'posts'
pattern: 'posts/*.md',
sortBy: postCreation,
},
pages: {
pattern: ['**/*.md','!posts/*.md'],
},
}))
.use(collectionIndexes({
'posts/index.html': {
collection: 'posts',
layout: 'index.hbs',
},
'index.html': {
collection: 'pages',
layout: 'sitemap.hbs',
},
}))
.use(markdown())
.use(addMeta({
config: config.theme,
title: (d) => pathToTitle(d.path),
longtitle: (d) => pathToTitle(d.path, false),
layout: (d) => layoutFromCollection(d),
filename: (d, f) => f,
git: (d) => gitMeta[d.path],
filename: (d, f) => {
// set path to file path, set filename to original path
d._path = d.path;
d.path = f;
return d._path;
},
git: (d) => gitMeta[d._path],
}))
.use(discoverPartials({
directory: 'partials',
directory: 'theme/partials',
pattern: /\.hbs$/
}))
.use(discoverHelpers({
directory: 'helpers',
pattern: /\.js$/
}))
.use(layouts({
default: 'default.hbs',
default: 'page.hbs',
directory: 'theme',
engineOptions: {
helpers: helpers,
},
}))
.use(forFiles((d,f) => buildlog.silly(f,elideMeta(d))))
.use(assets());
.use(assets({
src: 'theme/assets',
}));
};
if (require.main === module) {
// Called directly, build
module.exports().build(function(err) {
if (!err) return;
console.error('Build failed: '+err);
console.error('Build failed: ', err);
process.exit(1);
});
}
......
'use strict';
const express = require('express');
const fs = require('fs');
const emailTemplates = require('./lib/email-templates.js');
const metalsmith = require('./metalsmith.js');
const email = require('./lib/email.js');
const git = require('./lib/git.js')({});
......@@ -39,7 +40,6 @@ var fileOptions = {
}
};
async function build() {
buildlog.info('starting build...')
metalsmith().build(function(err) {
......@@ -49,17 +49,19 @@ async function build() {
buildlog.info('completed build.')
}
app.set('trust proxy', true); // Assume we're behind a proxy
app.use(express.static('/', expressOptions));
app.use(express.json());
app.use(express.urlencoded({extended: true}));
app.all('*', (req, res, next) => {
next();
httplog.info(`${req.method} ${req.url} ${req.ip} ${res.statusCode}`, {
httplog.info(`${req.method} ${req.url} ${req.hostname} ${req.ip} ${res.statusCode}`, {
method: req.method,
url: req.url,
ip: req.ip,
code: res.code,
host: req.hostname,
code: res.statusCode,
});
})
app.get('/', (req, res) => {
......@@ -118,13 +120,52 @@ app.post('/registration', (req, res) => {
submitlog.info("error emailing registration notification, ",
err);
}
submitlog.info('emailing registration notification: ', req.body);
// Send notification of registration to admins
const sendConfig = config.email.registration || {};
send(sendConfig,
`<pre>${JSON.stringify(req.body, null, 2)}</pre>`)
.then(ok, error);
const templateContext = {
config: {theme: config.theme},
request: req
};
submitlog.info(`emailing registration notification to ${sendConfig.to}`, req.body);
const notificationConfig = {
from: sendConfig.from,
to: sendConfig.to,
subject: sendConfig.notification,
};
try {
const html = emailTemplates.notification(templateContext);
send(notificationConfig,
html,
req.body)
.then(ok, error);
}
catch(e) {
maillog.error(`failed to send notification of registration to ${notificationConfig.to}: `, e);
}
if (req.body.email) {
// Send acknowledgement email to registrant
const acknowledgementConfig = {
from: sendConfig.from,
to: req.body.email,
subject: sendConfig.acknowledgement,
};
try {
const html = emailTemplates.acknowledgement(templateContext);
submitlog.info(`emailing registration acknowledgement to ${acknowledgementConfig.to}: `);
send(acknowledgementConfig,
html)
.then(ok, error);
}
catch(e) {
maillog.error(`failed to send registration acknowledgement to ${acknowledgementConfig.to} `, e);
}
}
else {
maillog.error(`cannot send registration acknowledgement, no email set in registration`);
}
res.redirect('/home');
});
......
assets/sass/.sass-cache
/node_modules/
Copyright (c) 2013 Rory Gibson - Released under The MIT License.
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.
\ No newline at end of file
# Ghostwriter
Ghostwriter is a simple AJAX driven theme for the [Ghost](http://github.com/tryghost/ghost/) blogging platform released under the MIT License.
**[View a demo of Ghostwriter](http://ghost.jollygoodthemes.com/ghostwriter/)**
**[Download Ghostwriter](https://github.com/roryg/ghostwriter/archive/master.zip)**
**[See more of my themes](http://jollygoodthemes.com)**
### Features
* Supports Ghost 0.5.x
* Clean typography focused design.
* Fully responsive (includes fitvid.js for responsive video embeds).
* AJAX loading for fast, smooth transitions between posts and pages.
* SASS files included.
* Static page support.
* Featured post support.
* A bunch of other bits and bobs.
### Troubleshooting
###### The Browse Posts link isn't working
If you're having trouble with the browse posts link not working make sure you're using the currect URL for your environment in your Ghost config.js file. I.e. if your Ghost site is being served on http://your-site-name.com make sure you use exactly that URL in your config.js file and not for example http://www.your-site-name.com
### Screenshot
![Ghostwriter home page](https://github.com/roryg/ghostwriter/blob/master/screenshot.png?raw=true)
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS and IE text size adjust after device orientation change,
* without disabling user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability of focused elements when they are also in an
* active/hover state.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {