Initial commit
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
docker-cache/
|
||||||
|
node_modules/
|
||||||
|
distribution/
|
||||||
|
assets/css/
|
||||||
|
assets/js/
|
||||||
|
assets/images/webp/
|
||||||
|
assets/images/avif/
|
||||||
|
docker-compose.yml
|
102
.github/workflows/build_docker_image.yml
vendored
Normal file
102
.github/workflows/build_docker_image.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
name: App Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_app_image:
|
||||||
|
name: Build app image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DH_REGISTRY: docker.io
|
||||||
|
GH_REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
REPOSITORY: ${{ github.event.repository.name }}
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DH_REGISTRY }}
|
||||||
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.GH_REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for release Docker image
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.DOCKER_USERNAME }}/${{ env.REPOSITORY }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Extract metadata for develop Docker image
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
|
id: meta-develop
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and push release Docker image
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
target: deploy
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha,scope=${{ github.workflow }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
|
||||||
|
|
||||||
|
- name: Build and push develop Docker image
|
||||||
|
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
target: deploy
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-develop.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-develop.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha,scope=${{ github.workflow }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
|
27
.github/workflows/npm-audit.yml
vendored
Normal file
27
.github/workflows/npm-audit.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: NPM Audit Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 16 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
npm_audit:
|
||||||
|
name: Check NPM audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: NPM Audit
|
||||||
|
run: npm audit
|
110
.gitignore
vendored
Normal file
110
.gitignore
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
assets/css/
|
||||||
|
assets/js/
|
||||||
|
assets/webp/
|
||||||
|
config/
|
||||||
|
distribution/
|
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${workspaceFolder}/distribution/index.js",
|
||||||
|
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||||
|
"outFiles": ["${workspaceFolder}/distribution/**/*.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:lts-alpine AS base
|
||||||
|
|
||||||
|
FROM base AS build-env
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN apk add --no-cache libwebp libwebp-tools libavif-apps
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npx tsc && npm run-script build && \
|
||||||
|
npm ci --only=production && \
|
||||||
|
ln -sf /usr/share/fonts assets/ && \
|
||||||
|
chown -R node .
|
||||||
|
|
||||||
|
FROM base AS deploy
|
||||||
|
|
||||||
|
HEALTHCHECK --timeout=3s \
|
||||||
|
CMD curl --fail http://localhost:8080/healthcheck || exit 1
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
COPY --from=build-env /usr/src/app /usr/src/app
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
CMD [ "node", "distribution/index.js"]
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nodejs-web:
|
||||||
|
container_name: nodejs-web
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: "no"
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
1761
package-lock.json
generated
Normal file
1761
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "nodejs-web-template",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A nodejs web project",
|
||||||
|
"keywords": [
|
||||||
|
"web",
|
||||||
|
"template"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/CorySanin/nodejs-web-template#readme",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/CorySanin/nodejs-web-template/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/CorySanin/nodejs-web-template.git"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Cory Sanin",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./distribution/index.js",
|
||||||
|
"files": [
|
||||||
|
"distribution",
|
||||||
|
"assets",
|
||||||
|
"views"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"ejs": "3.1.10",
|
||||||
|
"express": "5.1.0",
|
||||||
|
"express-session": "1.18.2",
|
||||||
|
"json5": "2.2.3",
|
||||||
|
"ky": "1.11.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sindresorhus/tsconfig": "8.0.1",
|
||||||
|
"@types/express": "^5.0.3",
|
||||||
|
"@types/express-session": "^1.18.2",
|
||||||
|
"@types/node": "^24.7.0",
|
||||||
|
"forking-build-shit": "1.0.4",
|
||||||
|
"typescript": "5.9.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "npx build-shit",
|
||||||
|
"watch": "npx build-shit --watch"
|
||||||
|
}
|
||||||
|
}
|
103
src/Web.ts
Normal file
103
src/Web.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as http from "http";
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import express from 'express';
|
||||||
|
import session from 'express-session';
|
||||||
|
import ky from 'ky';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import type { Express } from 'express';
|
||||||
|
|
||||||
|
interface WebConfig {
|
||||||
|
sessionSecret?: string;
|
||||||
|
port?: number;
|
||||||
|
secure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I still hate typescript.
|
||||||
|
*/
|
||||||
|
function notStupidParseInt(v: string | undefined): number {
|
||||||
|
return v === undefined ? NaN : parseInt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Web {
|
||||||
|
private _webserver: http.Server | null = null;
|
||||||
|
private app: Express | null = null;
|
||||||
|
private port: number;
|
||||||
|
private options: WebConfig;
|
||||||
|
|
||||||
|
constructor(options: WebConfig = {}) {
|
||||||
|
this.options = options;
|
||||||
|
this.port = notStupidParseInt(process.env['PORT']) || options['port'] as number || 8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
const options = this.options;
|
||||||
|
const sessionSecret = process.env['SESSIONSECRET'] || options.sessionSecret;
|
||||||
|
const app: Express = this.app = express();
|
||||||
|
|
||||||
|
if(!sessionSecret) {
|
||||||
|
console.error('sessionSecret is required.');
|
||||||
|
throw new Error('sessionSecret is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('view options', { outputFunctionName: 'echo' });
|
||||||
|
app.use('/assets', express.static('assets', { maxAge: '30 days' }));
|
||||||
|
app.use(session({
|
||||||
|
name: 'sessionId',
|
||||||
|
secret: sessionSecret,
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: undefined,
|
||||||
|
cookie: {
|
||||||
|
maxAge: notStupidParseInt(process.env['COOKIETTL']) || 1000 * 60 * 60 * 24 * 30, // 30 days
|
||||||
|
httpOnly: true,
|
||||||
|
secure: !!options.secure
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
app.use((_req, res, next) => {
|
||||||
|
crypto.randomBytes(32, (err, randomBytes) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
next(err);
|
||||||
|
} else {
|
||||||
|
res.locals['cspNonce'] = randomBytes.toString("hex");
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/healthcheck', (_, res) => {
|
||||||
|
res.send('Healthy');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (_, res) => {
|
||||||
|
res.render('index', {
|
||||||
|
page: {
|
||||||
|
title: 'Web',
|
||||||
|
titlesuffix: 'Home',
|
||||||
|
description: 'Homepage'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/ky', async (_, res) => {
|
||||||
|
res.send(await (await ky.get('https://sanin.dev')).text());
|
||||||
|
});
|
||||||
|
|
||||||
|
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
if (this._webserver) {
|
||||||
|
this._webserver.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Web;
|
||||||
|
export { Web, notStupidParseInt };
|
||||||
|
export type { WebConfig };
|
28
src/index.ts
Normal file
28
src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import JSON5 from 'json5';
|
||||||
|
import { Web, type WebConfig } from './Web.js';
|
||||||
|
|
||||||
|
interface compositeConfig {
|
||||||
|
web?: WebConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readConfig(): Promise<compositeConfig> {
|
||||||
|
try {
|
||||||
|
return JSON5.parse(await fs.promises.readFile(process.env['config'] || process.env['CONFIG'] || path.join(process.cwd(), 'config', 'config.jsonc'), 'utf-8'));
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('No config file found, using default config');
|
||||||
|
console.error(err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: compositeConfig = await readConfig();
|
||||||
|
|
||||||
|
const web = new Web(config.web);
|
||||||
|
await web.initialize();
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
web.close();
|
||||||
|
});
|
48
styles/00-reset.css
Normal file
48
styles/00-reset.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@sindresorhus/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
22
views/head.ejs
Normal file
22
views/head.ejs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<title><%= page.title %> | <%= page.titlesuffix %></title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="og:title" content="<%= page.title%> | <%= page.titlesuffix%>" />
|
||||||
|
<% if (page.description) { %>
|
||||||
|
<meta name="description" content="<%= page.description%>" />
|
||||||
|
<meta name="og:description" content="<%= page.description%>" />
|
||||||
|
<% } %>
|
||||||
|
<% if (page.image) { %>
|
||||||
|
<meta name="og:image" content="<%= page.image%>"/>
|
||||||
|
<link rel="image_src" href="<%= page.image%>"/>
|
||||||
|
<% } %>
|
||||||
|
<% if (page.canonical) { %>
|
||||||
|
<link rel="canonical" href="<%= page.canonical%>"/>
|
||||||
|
<% } %>
|
||||||
|
<link rel="shortcut icon" href="/assets/svg/favicon.svg">
|
||||||
|
<link rel="stylesheet" href="/assets/css/styles.css?v4">
|
||||||
|
<script nonce="<%= cspNonce %>">
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
document.body.classList.remove('preload');
|
||||||
|
});
|
||||||
|
</script>
|
14
views/index.ejs
Normal file
14
views/index.ejs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<%- include("head", locals) %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="preload">
|
||||||
|
<%- include("navigation", locals) %>
|
||||||
|
<div class="content">
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
views/navigation.ejs
Normal file
11
views/navigation.ejs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="navigation">
|
||||||
|
<div class="nav_logo">
|
||||||
|
<a href="/"><img src="/assets/svg/logo.svg" alt="Logo" /></a>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul class="nav_links">
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/ky">Away</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
Reference in New Issue
Block a user