Reusable static association and culture website with an optional Node API for contact forms, an Instagram live feed, and a local website editor. The repository contains generic examples only. Deployment-specific content stays in ignored local overrides.
public/contains the website, assets, pages, and JSON data.ui-web-edit-tool/contains the local admin/editor for content maintenance.server/contains the public API, editor API, and local server integration.tests/unit/contains fast Node tests.tests/e2e/contains browser smoke tests, including the editor workflow.docs/contains operations, testing, and content-maintenance notes.scripts/check-public-release.jschecks that local content is not published by accident.
npm install
npm run devnpm install installs all dependencies locally into node_modules/. No globally installed Node packages are required. The website then runs at http://localhost:8080 by default.
npm run security:setup-editor
npm run editorOpen the editor at http://127.0.0.1:8081/editor/ and log in with the local admin password. The editor only runs in EDITOR_MODE=1 with EDITOR_LOCAL_ONLY=true on 127.0.0.1, writes only allowlisted content files, and generates event artifacts from structured data.
With Docker, the default service starts the public website only. To run the local editor in Docker, first generate editor credentials and then start the opt-in editor profile:
npm run security:setup-editor
mkdir -p .editor-backups
docker compose up -d --build
docker compose --profile editor up -d --build editorThe normal website and the editor are separate services:
Website: http://127.0.0.1:8080
Editor: http://127.0.0.1:8081/editor/
Run only docker compose up -d --build when you want the public website only. Run both Docker commands above when you want the website and editor at the same time. The editor profile is bound to host localhost only and writes through bind mounts into your local public/ files.
Stop only the Docker editor without stopping the public website:
docker compose --profile editor stop editorStop the public website too:
docker compose downImportant content files:
public/data/site.local.jsonpublic/data/instagram-feed.local.jsonpublic/data/pages.local.jsonpublic/data/culture.local.jsonpublic/data/events.content.jsoncontains local event data and is not committed.public/data/events.default.jsonis the committed generic template with every supported event field.public/pages/events/*.htmlis generated locally for event detail pages and is not committed.
Deployment-specific media under public/assets/images/, public/assets/docs/, public/assets/videos/, and almost all files under public/assets/icons/ are ignored too. They remain usable locally but are not part of the reusable GitHub repository. The tracked placeholder public/assets/icons/site-placeholder.svg keeps a clean checkout renderable.
Events are maintained in the editor. Without a local event file, the website shows the generic examples from events.default.json; the first editor save creates events.content.json. Active and archived events use the same data shape. Active events can embed a safe HTTPS ticket shop as an iframe, keep the external ticket link as a fallback, and optionally set a bounded iframe height with a compact 316 pixel default. Archiving automatically disables ticket data; galleries remain available and are highlighted on archive pages.
Contact links are configured in site.local.json under contactSection.socialLinks, next to contactSection.instagramLabel. Visible Instagram preview cards are loaded live through the included server endpoint first. instagram-feed.local.json is used only when Instagram is unreachable. Details: docs/instagram.md.
The editor groups related fields as page elements: link text and target URL, for example, are edited together. The Pages view filters by homepage, subpage, and section; an explicit All pages mode remains available for global searches. The preview follows the selected page.
Under Settings, safe theme tokens such as colors, content width, radius, spacing, font style, and subtle effects can be adjusted. Changes appear in the preview first and are only saved to site.theme with Save settings. Arbitrary CSS is intentionally not accepted.
The Media section sorts images by event and archive folders. A folder filter like the Pages view shows Homepage gallery or a specific event folder. Event galleries expose the target event clearly. Each media card has four explicit actions: add to event gallery, add to main gallery, remove from event gallery, and remove from main gallery. Inapplicable actions are disabled. Removal requires confirmation and never deletes the media file.
The public site navigation stays visible at the top and automatically collapses into a dropdown when configured links no longer fit side by side. Local projects can add or rename navigation items without manually adjusting responsive breakpoints.
npm install --omit=dev
npm run startOr run it containerized on your own server or Raspberry Pi:
docker compose up -d --buildThe Compose service binds to 127.0.0.1:8080 by default and belongs behind an HTTPS reverse proxy.
Required environment variables:
NODE_ENV=production
TRUST_PROXY=loopback
SMTP_HOST=smtp.example.org
SMTP_PORT=587
SMTP_SECURE=false
SMTP_REQUIRE_TLS=true
SMTP_AUTH_METHOD=LOGIN
SMTP_USER=mail-user
SMTP_PASS=mail-password
SMTP_FROM=website@example.org
CONTACT_TO=contact@example.org
CAPTCHA_PROVIDER=hcaptcha
HCAPTCHA_SITE_KEY=public-site-key
HCAPTCHA_SECRET=private-secret
CONTACT_ALLOW_WITHOUT_CAPTCHA=0Optional:
PORT=8080Port 587 uses STARTTLS. SMTP_SECURE=true is only correct for port 465.
For Gmail, SMTP_PASS must be a Google App Password, not the normal Google password. Gmail app passwords copied with spaces are normalized before SMTP login.
If another SMTP provider rejects correct credentials with AUTH PLAIN, set SMTP_AUTH_METHOD=LOGIN. Without a value, Nodemailer chooses automatically.
Test SMTP access in isolation:
npm run smtp:checkCAPTCHA modes:
- No CAPTCHA: leave CAPTCHA env vars empty. In production, this is allowed for SMTP contact mail only when
CONTACT_ALLOW_WITHOUT_CAPTCHA=1is set deliberately and upstream abuse protection exists. - hCaptcha: set
CAPTCHA_PROVIDER=hcaptcha,HCAPTCHA_SITE_KEY, andHCAPTCHA_SECRET. - mCaptcha: set
CAPTCHA_PROVIDER=mcaptcha,MCAPTCHA_VERIFY_URL,MCAPTCHA_SITE_KEY,MCAPTCHA_SECRET, andMCAPTCHA_WIDGET_URL.MCAPTCHA_SCRIPT_URLis optional and defaults to the published vanilla glue script.
For Node/Docker deployments, configure CAPTCHA only in .env; the server injects the public browser config into site*.json and pages*.json at request time. For static-only hosting, configure the browser-visible captcha object in ignored local JSON because there is no server env at runtime.
For Docker, the website still starts with docker compose up -d --build. An optional mCaptcha profile is available for local experiments with docker compose --profile mcaptcha up -d mcaptcha mcaptcha-db; production mCaptcha should run behind TLS through a reverse proxy, or use the official upstream mCaptcha Compose deployment and point this site at it.
npm run release:check-public
npm run check
npm test
npm run test:e2e
npm audit --omit=devBefore production deployments and before the first public push, all commands should pass. npm run release:check combines the publish guard, syntax checks, unit tests, and E2E tests.
POST /api/contactGET /api/instagram-feed?username=xxxx&limit=4self-hosted Instagram live feedGET /.netlify/functions/instagram-feed?username=xxxx&limit=4Netlify-compatible Instagram live feedGET /healthzGET /editor/only innpm run editormode or the Dockereditorprofile
Netlify is not required for the static website. For live Instagram on a static host, use the included Netlify Function or an equivalent server-side endpoint. Contact forms need Netlify Forms or the Node API.
- The public website stays readable without login.
/editorand/api/editor/**are only directly locally reachable and protected by password, session cookie, CSRF checks, forwarded-header rejection, login lockouts, and rate limits.- Security headers, content sanitizing, URL allowlisting, upload signature checks, and API limits reduce common web risks.
.envcontains local secrets and must not be committed.- Local JSON overrides, generated event pages, and deployment-specific media are intentionally ignored.
The full operations guide, rotation steps, and residual risks are in docs/SECURITY.md. For larger new features, update this README with a short summary and add an operations detail document under docs/ when relevant.
This repository is available for non-commercial use under the PolyForm Noncommercial License 1.0.0. Attribution to SMNFL Github page is required. See LICENSE.md.
- Run
npm run release:check-public. - Check
git status --short --ignored:.env,public/data/*.local.json,public/data/events.content.json,public/pages/events/, and deployment-specific media must appear as ignored. - Review the generic defaults under
public/data/*.default.jsonand examples underpublic/data/*.example.json. - Run the tests and audit from the
Testssection. - Before committing, confirm that only the expected source, documentation, and example files are staged.
Details about the split between the reusable repository and private deployment content are in docs/deployment.md.