Public Access
1
0

feat: add proxy middleware for API requests and create production Docker Compose setup

This commit is contained in:
2025-08-23 11:27:40 -04:00
parent abfac06908
commit 0327ef81a6
5 changed files with 142 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
module.exports = function(eleventyConfig) { module.exports = function(eleventyConfig) {
const { createProxyMiddleware } = require('http-proxy-middleware');
// Pass through static assets from the "src" directory // Pass through static assets from the "src" directory
eleventyConfig.addPassthroughCopy("src/css"); eleventyConfig.addPassthroughCopy("src/css");
@@ -42,6 +43,21 @@ module.exports = function(eleventyConfig) {
// Add a shortcode for the current year for the footer // Add a shortcode for the current year for the footer
eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`); eleventyConfig.addShortcode("year", () => `${new Date().getFullYear()}`);
// Only add the proxy middleware when in serve mode
if (process.env.ELEVENTY_RUN_MODE === "serve") {
// Dev server options
eleventyConfig.setServerOptions({
// When the site is served by Eleventy's dev server, proxy API requests
// to the API server which is running in a separate container.
middleware: [
createProxyMiddleware('/api', {
target: 'http://api:3000', // The 'api' service container
changeOrigin: true,
}),
],
});
}
return { return {
// Set the source and output directories // Set the source and output directories
dir: { dir: {

20
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,20 @@
# docker-compose.prod.yml for production
# This setup builds and runs the optimized production image.
#
# To start, run: docker-compose -f docker-compose.prod.yml up --build
#
services:
app:
build:
context: .
target: production # Use the 'production' stage from the Dockerfile
container_name: eleventy_prod
ports:
- "3000:3000"
volumes:
# Persist the view count data in a named volume.
- views_data:/app/_data
restart: unless-stopped
volumes:
views_data:

103
package-lock.json generated
View File

@@ -12,7 +12,8 @@
"@11ty/eleventy": "^2.0.1", "@11ty/eleventy": "^2.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.21.2", "express": "^4.21.2",
"fs-extra": "^11.3.1" "fs-extra": "^11.3.1",
"nodemon": "^3.1.0"
} }
}, },
"node_modules/@11ty/dependency-tree": { "node_modules/@11ty/dependency-tree": {
@@ -1190,6 +1191,14 @@
"uglify-js": "^3.1.4" "uglify-js": "^3.1.4"
} }
}, },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1278,6 +1287,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -1846,6 +1860,53 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
}, },
"node_modules/nodemon": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nodemon/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/nodemon/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2076,6 +2137,11 @@
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
}, },
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
},
"node_modules/pug": { "node_modules/pug": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz",
@@ -2545,6 +2611,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
@@ -2601,6 +2678,17 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -2636,6 +2724,14 @@
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==" "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
}, },
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -2665,6 +2761,11 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",

View File

@@ -14,6 +14,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.21.2", "express": "^4.21.2",
"fs-extra": "^11.3.1", "fs-extra": "^11.3.1",
"nodemon": "^3.1.0" "nodemon": "^3.1.0",
"http-proxy-middleware": "^3.0.0"
} }
} }

View File

@@ -3,12 +3,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (viewCountElement) { if (viewCountElement) {
const slug = viewCountElement.dataset.slug; const slug = viewCountElement.dataset.slug;
const serverUrl = 'http://localhost:3000'; // IMPORTANT: Replace with your server's IP/domain
// Function to fetch and display the view count // Function to fetch and display the view count
const getViews = async () => { const getViews = async () => {
try { try {
const response = await fetch(`${serverUrl}/api/views/${slug}`); const response = await fetch(`/api/views/${slug}`);
if (!response.ok) throw new Error('Failed to fetch views'); if (!response.ok) throw new Error('Failed to fetch views');
const data = await response.json(); const data = await response.json();
viewCountElement.textContent = `${data.count} views`; viewCountElement.textContent = `${data.count} views`;
@@ -24,7 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Use a flag in localStorage to prevent incrementing on every refresh // Use a flag in localStorage to prevent incrementing on every refresh
const viewed = localStorage.getItem(`viewed-${slug}`); const viewed = localStorage.getItem(`viewed-${slug}`);
if (!viewed) { if (!viewed) {
await fetch(`${serverUrl}/api/views/${slug}`, { method: 'POST' }); await fetch(`/api/views/${slug}`, { method: 'POST' });
localStorage.setItem(`viewed-${slug}`, 'true'); localStorage.setItem(`viewed-${slug}`, 'true');
} }
} catch (error) { } catch (error) {