diff --git a/.env.example b/.env.example index e779af67..0782e9ba 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,27 @@ # You will need a .env file to use with the Docker containers. This is set up for localhost use with the Docker Compose fullstack. +# APP_HTTP_LISTEN_PORT is the TCP port used to access the Node application's HTTP interface (usually by a reverse proxy). +APP_HTTP_HOST="nm3clol-express-app" +# APP_HTTP_LISTEN_PORT is the TCP port used to access the Node application's HTTP interface (usually by a reverse proxy). +APP_HTTP_PORT=3000 +# APP_URL is the URL used to access the Node application (usually by a reverse proxy). +APP_HTTP_URL="http://${APP_HTTP_HOST}:${APP_HTTP_PORT}" + # SITE_NAME is used for page generation. SITE_NAME="(dev) No Moss 3 Carbo Landfill Online Localhost" # SITE_HOST is used for generating links for the search index. (If you leave this blank it should work using relative paths.) -SITE_HOST="localhost" +SITE_HOST="${APP_HTTP_HOST}" # SITE_URL is used for generating links for the search index. (If you leave this blank it should work using relative paths.) -SITE_URL="https://${SITE_HOST}" -# WELCOME_MSG is used for the homepage instead of "Welcome to ${SITE_NAME}!" -WELCOME_MSG="Devel' It Up, Developer!" +SITE_URL="${APP_HTTP_URL}" +# WELCOME_MESSAGE is used for the homepage instead of "Welcome to ${SITE_NAME}!" +SITE_WELCOME_MESSAGE="Devel' It Up, Developer!" -# APP_HTTP_LISTEN_PORT is the TCP port used to access the Node application's HTTP interface (usually by a reverse proxy). -APP_HTTP_HOST="nm3clol" -# APP_HTTP_LISTEN_PORT is the TCP port used to access the Node application's HTTP interface (usually by a reverse proxy). -APP_HTTP_LISTEN_PORT=3000 -# APP_URL is the URL used to access the Node application (usually by a reverse proxy). -APP_HTTP_URL="http://${APP_HTTP_HOST}:${APP_HTTP_LISTEN_PORT}" +# PUBLIC_DIR is the relative path to the directory to this project root for the public files +PUBLIC_DIR="../nm3clol-public" +# PAGES_DIR is the relative path to the directory to this project root for the pages rather than public files +PAGES_DIR="pages" +# STATIC_DIR is the relative path to the directory to this project root for the static asset files +STATIC_DIR="static" # SOLR_DOCS_HOST is the host for Apache Solr's core for indexed documents. SOLR_DOCS_HOST="solr" @@ -40,6 +47,3 @@ TIKA_HOST="tika" TIKA_PORT=9998 # TIKA_URL is the URL to access the host running Apache Tika. TIKA_URL="http://${TIKA_HOST}:${TIKA_PORT}" - -# PUBLIC_DIR is the directory relative to this project root for the public files -PUBLIC_DIR="../nm3clol-public" diff --git a/app/config.js b/app/config.js new file mode 100644 index 00000000..af794eae --- /dev/null +++ b/app/config.js @@ -0,0 +1,51 @@ +console.log(`Configuring .env and expanding .env to include environment variable references.`); + +const path = require('path'); +const dotenv = require('dotenv'); +const dotenvExpand = require('dotenv-expand'); + +let env = dotenv.config(); +dotenvExpand.expand(env); + +const getAppHttpHost = () => env.APP_HTTP_HOST||'nm3clol-express-app'; +const getAppHttpPort = () => parseInt(env.APP_HTTP_PORT||env.PORT||3000); +const getAppHttpUrl = () => { + return env.APP_HTTP_URL || `http://${getAppHttpHost() + ((getAppHttpPort() === 80) ? '' : ':' + getAppHttpPort())}` +}; + +const getSiteName = () => env.SITE_NAME||"(dev) No Moss 3 Carbo Landfill Online Localhost"; +const getSiteWelcomeMessage = () => env.SITE_WELCOME_MESSAGE||"Devel' It Up, Developer!"; +const getSiteHost = () => env.SITE_HOST||"localhost"; +const getSiteUrl = () => env.SITE_URL||getAppHttpUrl(); + +const getPublicPath = () => path.join(__dirname, '..', (env.PUBLIC_DIR||path.join('..', 'nm3clol-public')).replaceAll('\\', path.sep).replaceAll('/', path.sep)); +const getPagesPath = () => path.join(__dirname, '..', (env.PAGES_DIR||'pages').replaceAll('\\', path.sep).replaceAll('/', path.sep)); +const getStaticPath = () => path.join(__dirname, '..', (env.STATIC_DIR||'static').replaceAll('\\', path.sep).replaceAll('/', path.sep)); + +const getSolrDocsHost = () => env.SOLR_DOCS_HOST||'solr'; +const getSolrDocsPort = () => parseInt(env.SOLR_DOCS_PORT||8983); +const getSolrDocsCore = () => env.SOLR_DOCS_CORE||'nm3clol_core'; +const getSolrLawHost = () => env.SOLR_LAW_HOST||getSolrDocsHost(); +const getSolrLawPort = () => parseInt(env.SOLR_LAW_PORT||getSolrDocsPort()); +const getSolrLawCore = () => env.SOLR_LAW_CORE||'vacode_core'; + +module.exports = { + config: { + publicPath: getPublicPath(), + pagesPath: getPagesPath(), + staticPath: getStaticPath(), + siteName: getSiteName(), + siteWelcomeMessage: getSiteWelcomeMessage(), + siteHost: getSiteHost(), + siteUrl: getSiteUrl(), + appHttpHost: getAppHttpHost(), + appHttpPort: getAppHttpPort(), + appHttpUrl: getAppHttpUrl(), + solrDocsHost: getSolrDocsHost(), + solrDocsPort: getSolrDocsPort(), + solrDocsCore: getSolrDocsCore(), + solrLawHost: getSolrLawHost(), + solrLawPort: getSolrLawPort(), + solrLawCore: getSolrLawCore(), + } +}; diff --git a/routes/search.js b/app/routes/search.js similarity index 95% rename from routes/search.js rename to app/routes/search.js index e660167f..7b15aeb8 100644 --- a/routes/search.js +++ b/app/routes/search.js @@ -1,9 +1,11 @@ +console.log(`Loading nm3clol-express-app search router module...`); + const express = require('express'); const router = express.Router(); const { parse, toString } = require('lucene'); const { createClient, Query } = require('solr-client'); -const solrConfig = { host: process.env.SOLR_DOCS_HOST||'solr', port: process.env.SOLR_DOCS_PORT||8983, core: process.env.SOLR_DOCS_CORE_NAME||'nm3clol_core' }; -const helpers = require('../views/helpers/functions'); +const { config } = require('../config'); +const helpers = require('../../views/helpers/functions'); router.get('/', (req, res) => { // Extract paging parameters from request query parameters @@ -37,7 +39,7 @@ router.get('/', (req, res) => { usePhraseHighlighter: true, }}); // Create a Solr client - const solrClient = createClient({ host: 'solr.services.cleveland.daball.me', port: 8983, core: 'my_core' }); + const solrClient = createClient({ host: config.solrDocsHost, port: config.solrDocsPort, core: config.solrDocsCore }); solrClient.search(solrQuery) .then(solrResponse => { //console.log(require('util').inspect(solrResponse, { showHidden: true, depth: null, colors: true })); diff --git a/app/server.js b/app/server.js index 4a0d264d..d312343f 100644 --- a/app/server.js +++ b/app/server.js @@ -1,3 +1,5 @@ +console.log(`Starting up nm3clol-express-app...`); + const express = require('express'); const axios = require('axios'); const app = express(); @@ -6,18 +8,13 @@ const path = require('path'); const glob = require('glob'); const matter = require('gray-matter'); const ejs = require('ejs'); +const { config } = require('./config'); const helpers = require('../views/helpers/functions'); -const search = require('../routes/search'); +const search = require('./routes/search'); const fs = require('fs'); -const dotenv = require('dotenv'); -const dotenvExpand = require('dotenv-expand'); -dotenv.config(); -dotenvExpand.expand(process.env); // const advancedSearch = require('../routes/advanced-search'); -const publicPath = path.join(__dirname, '..', process.env.PUBLIC_DIR.replaceAll('\\', path.sep).replaceAll('/', path.sep)); -// Port number for HTTP server -const port = process.env.PORT||3000; +console.log(`Running app configuration:`, config); // Set EJS as the view engine app.set('view engine', 'ejs'); @@ -41,14 +38,14 @@ app.use(express.json()); // res.send('Hello World!'); // }) -console.log("Setting route for /ads.txt"); -app.get('/ads.txt', (req, res) => { - res.setHeader("Content-Type", "text/plain"); - res.setHeader("Cache-Control", "no-cache"); - res.send(`google.com, pub-8937572456576531, DIRECT, f08c47fec0942fa0`); -}); +// console.log("Setting route for /ads.txt"); +// app.get('/ads.txt', (req, res) => { +// res.setHeader("Content-Type", "text/plain"); +// res.setHeader("Cache-Control", "no-cache"); +// res.send(`google.com, pub-8937572456576531, DIRECT, f08c47fec0942fa0`); +// }); -console.log("Setting route for /robots.txt"); +console.log(`Serving /robots.txt from memory.`); app.get('/robots.txt', (req, res) => { res.setHeader("Content-Type", "text/plain"); res.setHeader("Cache-Control", "no-cache"); @@ -63,25 +60,29 @@ Allow: / }); // Search endpoints -console.log("Setting routes for /search"); +console.log(`Serving /search using search router.`); app.use('/search', search.router); // app.use('/advanced-search', advancedSearch.router); // Endpoints for all the site's pages. -console.log("Scanning for pages to create routes"); -glob.globSync('pages/**/*.md', { - cwd: path.join(__dirname, '..'), +console.log(`Scanning for pages in ${config.pagesPath} to create routes.`); +glob.globSync('**/*.md', { + cwd: config.pagesPath, matchBase: true, follow: true, }).forEach((filePath) => { const expressRoutePathFromFilePath = (filePath) => { - return filePath.substring('pages'.length, filePath.length - path.extname(filePath).length).replaceAll(path.sep, path.posix.sep); + filePath = filePath.substring(0, filePath.length - path.extname(filePath).length).replaceAll(path.sep, path.posix.sep); + if (!filePath.startsWith('/') && filePath.length > 0) { + filePath = `/${filePath}`; + } + return filePath; }; const route = expressRoutePathFromFilePath(filePath); - const fullFilePath = path.join(__dirname, '..', filePath); + const fullFilePath = path.join(config.pagesPath, filePath); let paths = route.split(path.posix.sep); paths[0] = 'public'; - console.log(`Setting route for ${route}`); + console.log(`Serving ${route} route as a page at ${fullFilePath}.`); app.get(route, async (req, res) => { const fm = matter.read(fullFilePath); const fmData = { fm: fm.data, excerpt: fm.excerpt }; @@ -91,7 +92,7 @@ glob.globSync('pages/**/*.md', { }); }); -// console.log("Scanning for documents to create routes"); +// console.log("Scanning for documents to create routes."); // glob.globSync('**/*{.pdf,.docx,.xlsx,.pptx,.doc,.xls,.ppt}', { // cwd: path.join(__dirname, '..', 'public'), // matchBase: true, @@ -114,7 +115,8 @@ glob.globSync('pages/**/*.md', { // }); // }); -console.log("Scanning for web archive HTML documents to create routes"); +//TODO: Rewrite this facility so that it utilizes Git index as a filesystem. +console.log("Scanning for web archive HTML documents to create routes."); glob.globSync('Web_Site_Archives/**/*{.htm,.html}', { cwd: path.join(__dirname, '..', 'public'), matchBase: true, @@ -136,8 +138,8 @@ glob.globSync('Web_Site_Archives/**/*{.htm,.html}', { }); -// Endpoints for all the site's YouTube videos. -console.log("Scanning for archived videos to create routes"); +//TODO: Rewrite this facility so that it utilizes Git index as a filesystem. +console.log("Scanning for archived videos to create routes."); glob.globSync(['Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.json', 'Virginia_Energy/YouTube_Archive/**/*.info.json', 'Virginia_Governor/**/*.info.json'], { cwd: path.join(__dirname, '..', 'public'), matchBase: true, @@ -187,10 +189,10 @@ glob.globSync(['Russell_County/Board_of_Supervisors/YouTube_Archive/**/*.info.js //app.get('/OCR-Encoded-PDFs/Russell-County-Web-Site_2024-02-13_19_50_Modified-With-OCR-Encoding**', rewriter.rewrite('/Web_Site_Archives/Russell_County_Web_Site-2024-02-13_19_50_Modified_With_OCR_Encoding/$1')); -console.log(`Setting routes for /vendor/**/*`);; +console.log(`Serving /vendor/**/* route for all files in ${path.join(config.staticPath, 'vendor')}`);; app.get('/vendor/**/*', async (req, res) => { await serve(req, res, { - public: path.join(__dirname, '..', 'static'), + public: config.staticPath, symlinks: true, trailingSlash: true, cleanUrls: false, @@ -204,10 +206,10 @@ app.get('/vendor/**/*', async (req, res) => { }); }); -console.log(`Setting routes for /css/*.css`);; +console.log(`Serving /css/*.css route for all files in ${path.join(config.staticPath, 'css')}`);; app.get('/css/*.css', async (req, res) => { await serve(req, res, { - public: path.join(__dirname, '..', 'static'), + public: config.staticPath, symlinks: true, trailingSlash: true, cleanUrls: false, @@ -221,10 +223,10 @@ app.get('/css/*.css', async (req, res) => { }); }); -console.log(`Setting routes for /svg/*.svg`);; +console.log(`Serving /svg/*.svg route for all files in ${path.join(config.staticPath, 'svg')}`);; app.get('/svg/*.svg', async (req, res) => { await serve(req, res, { - public: path.join(__dirname, '..', 'static'), + public: config.staticPath, symlinks: true, trailingSlash: true, cleanUrls: false, @@ -238,10 +240,11 @@ app.get('/svg/*.svg', async (req, res) => { }); }); -console.log(`Setting route for *`); +//TODO: Rewrite this facility so that it utilizes Git index as a filesystem. +console.log(`Serving * default route for all files in ${config.publicPath}`);; app.get('*', async (req, res) => { await serve(req, res, { - public: publicPath, + public: config.publicPath, symlinks: true, trailingSlash: true, cleanUrls: false, @@ -276,6 +279,11 @@ app.get('*', async (req, res) => { }); // Start server -app.listen(port, () => { - console.log(`no-moss-3-carbo-landfill-library.online app listening on port ${port}`); +app.listen(config.appHttpPort, () => { + console.log(`nm3clol-express-app HTTP server listening on port ${config.appHttpPort}.`) + console.log(`To access your app, you can use the localhost URL, http://localhost:${config.appHttpPort}.`); + console.log(`To access your app, you can use the 127.0.0.1 host, http://127.0.0.1:${config.appHttpPort}.`); + console.log(`To access your app, you can use the ::1 host, http://[::1]:${config.appHttpPort}.`); + console.log(`To access your app, you might can use the app host name, ${config.appHttpUrl}.`); + console.log(`This app is configured to use the web site URL, ${config.siteUrl}.`); }); diff --git a/views/directory.ejs b/views/directory.ejs index 680a9bc0..4040eb7a 100644 --- a/views/directory.ejs +++ b/views/directory.ejs @@ -18,9 +18,9 @@ <% if (h.shouldShowDirectorySeparator({index})) { %> <% } %> - <% if (h.shouldShowWelcomeBanner({paths})) { %> + <% if (h.shouldShowSiteWelcomeMessage({paths})) { %>   - <%= h.getWelcomeBanner() %> + <%= h.getSiteWelcomeMessage() %> <% } else if (h.shouldOmitLinkOnLastBreadcrumb({paths, index})) { %> <%= h.trimSlashes({path: value.name}).replaceAll('_', ' ') %> <% } else { %> diff --git a/views/helpers/functions.js b/views/helpers/functions.js index 1fcba0d8..753e0d43 100644 --- a/views/helpers/functions.js +++ b/views/helpers/functions.js @@ -1,7 +1,7 @@ const path = require('path'); const glob = require('glob'); const fs = require('fs'); -const process = require('process'); +const config = require('../../app/config'); const markdownit = require('markdown-it'); var markdownItAttrs = require('markdown-it-attrs'); const md = markdownit({ @@ -18,9 +18,7 @@ const md = markdownit({ ); const moment = require('moment-timezone').tz.setDefault("UTC"); -const getSiteName = () => { - return process.env.SITE_NAME || '(dev) No Moss 3 Carbo Landfill Online Localhost'; -} +const getSiteName = config.getSiteName; const trimSlashes = ({path}) => { return path.replace(/^[\/\\]|[\/\\]$/g, ''); @@ -40,11 +38,9 @@ const getDirectoryTitle = ({directory}) => { .join(' - '); return (directory=="public") ? getSiteName() : `${title} - ${getSiteName()}`; }; -const getWelcomeBanner = () => { - return process.env.WELCOME_MSG || `Welcome to ${getSiteName()}!`; -}; +const getSiteWelcomeMessage = config.getSiteWelcomeMessage; const shouldShowDirectorySeparator = ({index}) => (index > 0); -const shouldShowWelcomeBanner = ({paths}) => (paths.length == 1); +const shouldShowSiteWelcomeMessage = ({paths}) => (paths.length == 1); const shouldOmitLinkOnLastBreadcrumb = ({paths, index}) => (index == paths.length-1); const resolveReadmeFile = ({directory}) => { @@ -88,7 +84,7 @@ const renderArchive = (html, paths) => { // Header and Footer content const headHeaderContent = ``; const headFooterContent = ` - +