npm install
npm run local
When running locally, this application uses an .env
file.
Please see the section on working with this application for more details.
To deploy this application to the beta appspot service:
gcloud app deploy beta.yaml
To deploy this application to the prod appspot service (with optional --quiet
flag to expedite the deploy):
gcloud app deploy app.yaml --quiet
This application is designed to allow content edits to happen with ease, but allow enough flexibility to accommodate significant deviations from the templated methods.
# General node.js env vars
PORT=8080 #port to run on
NODE_ENV=development
ENV_NAME=local #Give your instance a distinct name that surfaces in the header
# Settings for configuring behind a reverse proxy
PROXY_URL=local.domain
HOST=0.0.0.0
DISABLE_SSL=true
# Settings for signed cookies
COOKIE_SECRET=secret
# GCP-specific env vars for external service communication
GOOGLE_CLOUD_REGION=us-east1
GOOGLE_APPLICATION_CREDENTIALS=/path/to/application_default_credentials.json
# Publication configuration
PUBLICATION_ID=publisher-center-ppid.google.com
OAUTH_CLIENT_ID=abcd-1234.apps.googleusercontent.com
OAUTH_CLIENT_SECRET=secret-abc-1234
GOOGLE_SITE_VERIFICATION=public-abc-1234
# Google Analytics
GTAG_PROPERTY_ID=G-1234567890 #Configure gTag Id
GTAG_DEBUG_MODE=true #Enable 'debug' mode in GA for streaming all events
# SwG Environments
# Setting this change the 'swg.js' script tag to be 'swg-another-value.js'
SWG_OVERRIDE=another-value
# By default, swg.js is initialized with subscriptions.configure({paySwgVersion:'2'})
# For SwG Classic publications, set this optional variable to a different version
PAY_SWG_VERSION='1' # For SwG Classic
# PAY_SWG_VERSION='2' # For RRM:E, default value of this variable is ommitted
# Prompt Configurations
# This value is a stringified JSON configuration of the following schema:
# { "TYPE_<prompt type>": [
# {"name": "Configuration_Name","configurationId":"1234"},
# ...
# ]}
PROMPT_CONFIG='{"TYPE_NEWSLETTER_SIGNUP":[{"name":"Newsletter_Signup","configurationId":"8bebde75-07e4-4cbc-8117-785435a30848"},{"name":"Breakin_News","configurationId":"d7c52c18-dcca-4ca3-b4df-022c557b06b8"}],"TYPE_REWARDED_SURVEY":[{"name":"Multiple_Questions","configurationId":"ef6def43-2565-4e5b-ad06-80ebecaa715e"}]}'
# If the current system cannot support string-encoded JSON as an environmental var,
# a base64-encoded version can be used:
PROMPT_CONFIG_BASE64=eyJUWVBFX05FV1NMRVRURVJfU0lHTlVQIjpbeyJuYW1lIjoiTmV3c2xldHRlcl9TaWdudXAiLCJjb25maWd1cmF0aW9uSWQiOiI4YmViZGU3NS0wN2U0LTRjYmMtODExNy03ODU0MzVhMzA4NDgifSx7Im5hbWUiOiJCcmVha2luX05ld3MiLCJjb25maWd1cmF0aW9uSWQiOiJkN2M1MmMxOC1kY2NhLTRjYTMtYjRkZi0wMjJjNTU3YjA2YjgifV0sIlRZUEVfUkVXQVJERURfU1VSVkVZIjpbeyJuYW1lIjoiTXVsdGlwbGVfUXVlc3Rpb25zIiwiY29uZmlndXJhdGlvbklkIjoiZWY2ZGVmNDMtMjU2NS00ZTViLWFkMDYtODBlYmVjYWE3MTVlIn0seyJuYW1lIjoiU2luZ2xlX1F1ZXN0aW9uIiwiY29uZmlndXJhdGlvbklkIjoiMDdmZWZlODMtOGFhOS00OGVjLWExNzItZmYwNTIyMjA5Y2Y0In1dfQ
This project is an express
-based node.js application, designed to run locally
or in a cloud runtime like App Engine or Glitch. It aims to make authoring
technical content in markdown
documents, while using the flexibility of
handlebars templates and partials to afford customization at the page level. As
it is a node.js app, apis can be hosted to be used within the documents'
examples.
├── app # Where articles are authored and where apis are hosted
├── docs # Community guidelines
├── lib # Shared libraries for use across many parts of the app
├── middleware # Custom middleware for running the app in specific circumstances
├── public # Public css and js
├── server.js # The actual app
Page rendering is done in two phases:
markdown
to html
)html
representations of articles are rendered as the body within a
handlebars templateArticles are located in /app/content
, and are rendered within templates found
in /app/views
. Articles can be either html
or markdown
, both of which can
include arbitrary in-line html
. The markdown
renderer includes extensions
for callouts, code blocks and custom ids (for use with client-side js).
Additionally, articles can include custom client-side javascript
, injected in
the head of the document as a deferred js module. For example, from
/app/views/layouts/demo-layout.html
:
{{ #data.script }}
<script defer type="module" src="{{ this }}"></script>
{{ /data.script }}
Custom per-page javascript
modules, and per-section page templates, are
specified in the nav for the section, e.g. /lib/nav/documentation.js
. See
Adding an article for more information.
Articles are found in the /app/content/
folder, and can be markdown
or
html
. When running the server locally, articles can be previewed by reloading
the page without the need to restart the server, as the app renders the articles
from markdown on every pageload.
To add a new article, the file must be created, added to the navigation and then configured with the relevant information within the navigation. While developing locally, each time a change is made to the navigation, the server must be restarted.
markdown
or html
file to app/content/
lib/nav/documentation.js
:The navigation file (e.g. /lib/navigation/documentation.js
) constains a json
object that roughly follows these rules (with in-line comments). All fields are
required unless specified:
[
{
"section": "Section label", // Label that appears for the section
"template": "app/views/layouts/demo-layout.html" // (optional) Handlebars template, defaults to demo-layout.html
"options": { //Optional section-level options
"suppressInNav": true //Hide this section from the nav, but preserve access directly
},
"links": [
{
"label": "Test", // Visible label
"url": "/", // Url (relative to root)
"content": "app/content/test.md" // .md or .html to render,
"script": "js/script.js" // Optional, relative to the /public folder
"options": { //Optional, passed directly to template rendering
"suppressStructuredDataMarkup" : true, // suppressed the ld+json block rendering
"suppressInNav": true, //Hide this route from this section, but preserve access
"lang": "en" //Specify a specific <html lang=""> value for a route
}
}
// Insert more links for this section
]
}
// Insert more sections for this navigation tree
]
!!! hint NOTE : if a template is not specified, the demo-layout.html
template is used. !!!
Adding a new section follows the rules of adding a new file, at least insofar as
modifying the navigation and restarting the server are concerned. That being
said, each section can define its own handlebars template, which in turn can
require their own partials. Handlebars templates are located in
/app/views/layouts/
and partials in /app/views/partials
.
script
to nav /lib/nav/documentation.js
/public/js/add-swg-button.js
#id
to use within client-side javascript. (see
/app/content/subscription-linking.md) (see
/app/content/subscription-linking.md)To add custom javascript to a page, and have it link to the markdown-rendered DOM, an easy approach is to begin with a specified header id, and use that to key into the page with.
This section uses the {#example}
custom id, and has the exampel.js
script
added to the script
entry in the nav. As a result, there is a button under the
header that is clickable and fires a custom event.
/lib/nav/documentation.js
entry{
section: "Content examples",
links: [
{
label: 'admonition callouts',
url: '/examples',
content: 'app/content/examples.md'
},
{
label: 'How to edit this site',
url: '/contributing',
content: 'README.md',
script: 'js/readme.js'
}
]
}
/public/js/readme.js
scriptfunction makeExampleButton() {
const button = document.createElement('button');
const label = document.createTextNode('Press me!');
button.appendChild(label);
button.addEventListener('click', (event)=>{
alert("Nice job!");
});
document
.querySelector('#example')
.insertAdjacentElement('afterend', button);
}
document.addEventListener('DOMContentLoaded', function () {
makeExampleButton();
});
To create a new back-end service, a new route should be created for it, attached at the appropriate path, and then used by client-side javascript.
In this example, the router is located at /app/routes/readme.js
import express from 'express';
const router = express.Router();
router.get('/', async (req, res, next) => {
return res.end(`Random number: ${Math.round(Math.random()*1000)}`);
});
export default router;
server.js
import readme from './app/routes/readme.js'
app.use('/readme', readme)
readme.js
Create a new function in your previously existing client-side js.
In this example: /public/js/readme.js
// New function to call the api
async function makeAPICall() {
const host = location.host;
const endpoint = `readme`;
const url = `https://${host}/${endpoint}`;
try {
return await fetch(url).then(r=>r.text());
} catch(e) {
throw new Error(`Unable to fetch ${url}`);
}
}
// Modified existing function to use the new function
function makeExampleButton() {
const button = document.createElement('button');
const label = document.createTextNode('Press me!');
button.appendChild(label);
button.addEventListener('click', async (event)=>{
const message = await makeAPICall();
alert(message);
});
document
.querySelector('#example')
.insertAdjacentElement('afterend', button);
}