feat: add wordpress site variant
@@ -4,9 +4,9 @@ Edit this file for each repository.
|
|||||||
|
|
||||||
## Project identity
|
## Project identity
|
||||||
|
|
||||||
Project name: `CHANGE_ME`
|
Project name: `Azione!Lab WordPress`
|
||||||
Project description: `CHANGE_ME`
|
Project description: `Single-page WordPress website for a contemporary theatre workshop.`
|
||||||
Primary language/runtime: `CHANGE_ME`
|
Primary language/runtime: `PHP 8.3 and WordPress 7`
|
||||||
|
|
||||||
## Project mode
|
## Project mode
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ Choose one:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
project_mode: personal
|
project_mode: personal
|
||||||
project_mode: work
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
@@ -29,8 +28,6 @@ Enable only the profiles that apply to this repository:
|
|||||||
```text
|
```text
|
||||||
enabled_profiles:
|
enabled_profiles:
|
||||||
- docker
|
- docker
|
||||||
- ansible
|
|
||||||
- python
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Branching model
|
## Branching model
|
||||||
@@ -77,7 +74,14 @@ All tests MUST be executed inside Docker containers.
|
|||||||
Configure the canonical test command for this repository:
|
Configure the canonical test command for this repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CHANGE_ME
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/functions.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/front-page.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/azionelab-content.php
|
||||||
|
docker compose run --rm --no-deps proxy nginx -t
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||||
|
docker compose config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Development-only placeholders. Replace all passwords outside local development.
|
||||||
|
MARIADB_DATABASE=azionelab
|
||||||
|
MARIADB_USER=azionelab
|
||||||
|
MARIADB_PASSWORD=replace-with-a-local-password
|
||||||
|
MARIADB_ROOT_PASSWORD=replace-with-a-local-root-password
|
||||||
|
WORDPRESS_TABLE_PREFIX=azl_
|
||||||
|
WORDPRESS_DEBUG=0
|
||||||
|
WP_ENVIRONMENT_TYPE=local
|
||||||
|
WP_URL=http://azionelab.org:8080
|
||||||
|
WP_TITLE=Azione!Lab
|
||||||
|
WP_ADMIN_USER=azionelab-admin
|
||||||
|
WP_ADMIN_PASSWORD=replace-with-a-local-admin-password
|
||||||
|
WP_ADMIN_EMAIL=admin@example.org
|
||||||
|
NGINX_BIND_ADDRESS=127.0.0.1
|
||||||
|
NGINX_HTTP_PORT=8080
|
||||||
|
NGINX_HTTPS_PORT=8443
|
||||||
|
TRUST_PROXY_HEADERS=0
|
||||||
|
LETSENCRYPT_ENABLED=0
|
||||||
|
LETSENCRYPT_DOMAIN=azionelab.org
|
||||||
|
LETSENCRYPT_EMAIL=operator@example.org
|
||||||
|
LETSENCRYPT_STAGING=1
|
||||||
|
LETSENCRYPT_RENEW_INTERVAL_SECONDS=43200
|
||||||
|
LETSENCRYPT_RETRY_SECONDS=300
|
||||||
|
TLS_RELOAD_INTERVAL_SECONDS=30
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.env
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
backups/
|
||||||
|
tests/functional/test-results/
|
||||||
|
tests/functional/playwright-report/
|
||||||
@@ -1,73 +1,126 @@
|
|||||||
# codex-bootstrap
|
# Azione!Lab WordPress
|
||||||
|
|
||||||
A repository template for AI-assisted development with Codex.
|
A warm, editorial single-page website for the Azione!Lab contemporary theatre
|
||||||
|
workshop. WordPress manages the content, a custom theme owns the visual system, and
|
||||||
|
Docker Compose provides MariaDB, NGINX, optional Let's Encrypt, WP-CLI, and isolated
|
||||||
|
browser tests.
|
||||||
|
|
||||||
This template defines a repeatable workflow for using Codex as an autonomous coding agent that can create branches, modify code, run Docker-based tests, update documentation, write ADRs, and commit changes using Conventional Commits.
|
## Architecture
|
||||||
|
|
||||||
## Purpose
|
- `wordpress/`: pinned WordPress 7/PHP 8.3 image, custom theme, and structured-content
|
||||||
|
must-use plugin.
|
||||||
|
- `db`: MariaDB 11.8 LTS, available only on the internal data network.
|
||||||
|
- `proxy`: the only published service; routes `azionelab.org` to WordPress and applies
|
||||||
|
edge security controls.
|
||||||
|
- `certbot`: optional HTTP-01 certificate issue/renewal service.
|
||||||
|
- `wp-cli`: opt-in bootstrap and maintenance service.
|
||||||
|
- `tests/functional`: Playwright tests running only through the public virtual host.
|
||||||
|
|
||||||
Use this template to bootstrap repositories where Codex must operate with clear rules, minimal changes, pragmatic TDD, security guardrails, and explicit documentation requirements.
|
See [architecture](docs/architecture.md) and
|
||||||
|
[ADR-0001](docs/adr/0001-wordpress-single-page.md).
|
||||||
|
|
||||||
## Repository structure
|
## Prerequisites
|
||||||
|
|
||||||
```text
|
- Docker Engine with Docker Compose v2.
|
||||||
.
|
- Local ports `8080` and `8443` available.
|
||||||
├── AGENTS.md
|
- `127.0.0.1 azionelab.org` in the local hosts file.
|
||||||
├── README.md
|
|
||||||
├── .codex/
|
No host PHP, database, Node.js, or WordPress installation is required.
|
||||||
│ ├── project.md
|
|
||||||
│ ├── workflow.md
|
## Start locally
|
||||||
│ ├── security.md
|
|
||||||
│ ├── quality.md
|
```bash
|
||||||
│ ├── orchestration.md
|
cp .env.example .env
|
||||||
│ ├── prompts/
|
docker compose up --build -d
|
||||||
│ │ ├── task.md
|
docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||||
│ │ ├── bugfix.md
|
docker compose ps
|
||||||
│ │ ├── refactor.md
|
|
||||||
│ │ ├── security-review.md
|
|
||||||
│ │ └── documentation.md
|
|
||||||
│ ├── agents/
|
|
||||||
│ │ ├── architect.md
|
|
||||||
│ │ ├── developer.md
|
|
||||||
│ │ ├── reviewer.md
|
|
||||||
│ │ ├── security-reviewer.md
|
|
||||||
│ │ ├── test-engineer.md
|
|
||||||
│ │ └── documentation-writer.md
|
|
||||||
│ └── profiles/
|
|
||||||
│ ├── docker.md
|
|
||||||
│ ├── ansible.md
|
|
||||||
│ └── python.md
|
|
||||||
└── docs/
|
|
||||||
├── adr/
|
|
||||||
│ └── 0000-template.md
|
|
||||||
├── architecture.md
|
|
||||||
├── deployment.md
|
|
||||||
├── operations.md
|
|
||||||
├── security.md
|
|
||||||
├── testing.md
|
|
||||||
└── runbook.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
Open:
|
||||||
|
|
||||||
1. Copy this template into a new or existing repository.
|
- site: <http://azionelab.org:8080>
|
||||||
2. Edit `.codex/project.md` and configure:
|
- admin: <http://azionelab.org:8080/wp-admin/>
|
||||||
- project mode;
|
|
||||||
- enabled profiles;
|
|
||||||
- Docker-based test command;
|
|
||||||
- branch naming rules if needed.
|
|
||||||
3. Add project-specific details to the documentation under `docs/`.
|
|
||||||
4. When asking Codex to work on a task, use one of the prompt templates under `.codex/prompts/`.
|
|
||||||
|
|
||||||
## Core rules
|
Use the development credentials copied into `.env` only locally. Change them before
|
||||||
|
sharing the environment.
|
||||||
|
|
||||||
Codex must:
|
## Edit content
|
||||||
|
|
||||||
- start work from `develop`;
|
- **Appearance > Customize**: hero, manifesto, laboratory, teacher, lessons, contacts,
|
||||||
- create a dedicated `feature/`, `fix/`, or `hotfix/` branch;
|
social links, and footer.
|
||||||
- use pragmatic TDD;
|
- **Shows**: poster, title, excerpt, year, place, and order.
|
||||||
- keep changes minimal and focused;
|
- **Gallery**: image, category, caption/title, and order.
|
||||||
- run the configured Docker-based test command before completion;
|
- **Settings > General**: site name and tagline.
|
||||||
- update documentation and ADRs when needed;
|
|
||||||
- produce a final report with summary, tests, risks, and rollback notes;
|
Real uploaded images replace the built-in editorial SVG placeholders. The three
|
||||||
- commit using Conventional Commits.
|
participation cards remain intentional theme copy; making them reorderable is a TODO if
|
||||||
|
the workshop later needs more than the current three fixed audiences.
|
||||||
|
|
||||||
|
## Production configuration
|
||||||
|
|
||||||
|
Set `WP_ENVIRONMENT_TYPE=production`, unique database passwords of at least 16
|
||||||
|
characters, a strong admin password, the public HTTPS `WP_URL`, and `WORDPRESS_DEBUG=0`.
|
||||||
|
Production startup rejects placeholder database credentials. WordPress file editing
|
||||||
|
and web-based theme/plugin/core modification are disabled; deploy updates by rebuilding
|
||||||
|
the pinned image.
|
||||||
|
|
||||||
|
When a trusted load balancer owns TLS, keep `LETSENCRYPT_ENABLED=0` and set
|
||||||
|
`TRUST_PROXY_HEADERS=1` only if NGINX is reachable exclusively from that load balancer
|
||||||
|
and the load balancer overwrites forwarding headers.
|
||||||
|
|
||||||
|
For direct exposure, point public DNS at the host, expose ports 80/443, and start with
|
||||||
|
the Let's Encrypt staging CA:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
WP_ENVIRONMENT_TYPE=production
|
||||||
|
WP_URL=https://azionelab.org
|
||||||
|
LETSENCRYPT_ENABLED=1
|
||||||
|
LETSENCRYPT_DOMAIN=azionelab.org
|
||||||
|
LETSENCRYPT_EMAIL=operator@example.org
|
||||||
|
LETSENCRYPT_STAGING=1
|
||||||
|
NGINX_BIND_ADDRESS=0.0.0.0
|
||||||
|
NGINX_HTTP_PORT=80
|
||||||
|
NGINX_HTTPS_PORT=443
|
||||||
|
TRUST_PROXY_HEADERS=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Before the first certificate exists, NGINX serves ACME challenges and returns 503 for
|
||||||
|
application traffic. After issuance, HTTP redirects to HTTPS automatically.
|
||||||
|
|
||||||
|
## Useful commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f proxy wordpress db certbot
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp plugin list'
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/functions.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/front-page.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/azionelab-content.php
|
||||||
|
docker compose run --rm --no-deps proxy nginx -t
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||||
|
docker compose config --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Back up the database and WordPress files at the same logical point in time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p backups/wordpress-files
|
||||||
|
docker compose stop wordpress
|
||||||
|
docker compose exec -T db sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' > backups/database.sql
|
||||||
|
docker compose cp wordpress:/var/www/html/. backups/wordpress-files/
|
||||||
|
docker compose start wordpress
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not commit backups or `.env`. Encrypt and test real backups off-host. See
|
||||||
|
[operations](docs/operations.md) before restoring or deleting volumes.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Revert the application commit and rebuild containers. Preserve `db_data`,
|
||||||
|
`wordpress_data`, and certificate volumes. Database restoration is a separate,
|
||||||
|
destructive operation and requires a verified backup.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
domain="${LETSENCRYPT_DOMAIN:-azionelab.org}"
|
||||||
|
email="${LETSENCRYPT_EMAIL:-}"
|
||||||
|
staging="${LETSENCRYPT_STAGING:-1}"
|
||||||
|
renew_interval="${LETSENCRYPT_RENEW_INTERVAL_SECONDS:-43200}"
|
||||||
|
retry_interval="${LETSENCRYPT_RETRY_SECONDS:-300}"
|
||||||
|
|
||||||
|
case "$domain" in "" | *[!A-Za-z0-9.-]* | .* | *. | *..*) echo "Invalid LETSENCRYPT_DOMAIN." >&2; exit 1 ;; esac
|
||||||
|
[ -n "$email" ] || { echo "LETSENCRYPT_EMAIL is required when Let's Encrypt is enabled." >&2; exit 1; }
|
||||||
|
case "$staging" in 0 | 1) ;; *) echo "LETSENCRYPT_STAGING must be 0 or 1." >&2; exit 1 ;; esac
|
||||||
|
case "$renew_interval:$retry_interval" in *[!0-9:]* | :* | *:) echo "Certbot intervals must be positive integers." >&2; exit 1 ;; esac
|
||||||
|
[ "$renew_interval" -gt 0 ] && [ "$retry_interval" -gt 0 ] || { echo "Certbot intervals must be positive." >&2; exit 1; }
|
||||||
|
|
||||||
|
staging_argument=""
|
||||||
|
[ "$staging" = 0 ] || staging_argument="--staging"
|
||||||
|
trap 'exit 0' TERM INT
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
if certbot certonly --webroot --webroot-path /var/www/certbot --preferred-challenges http \
|
||||||
|
--cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive \
|
||||||
|
--keep-until-expiring $staging_argument; then
|
||||||
|
delay="$renew_interval"
|
||||||
|
else
|
||||||
|
echo "Certificate request failed; retrying after ${retry_interval}s." >&2
|
||||||
|
delay="$retry_interval"
|
||||||
|
fi
|
||||||
|
sleep "$delay" &
|
||||||
|
wait $! || true
|
||||||
|
done
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
environment="${WP_ENVIRONMENT_TYPE:-local}"
|
||||||
|
case "$environment" in
|
||||||
|
local | development | staging) ;;
|
||||||
|
production)
|
||||||
|
if [ "$MARIADB_USER" = "root" ]; then
|
||||||
|
echo "MARIADB_USER must not be root in production." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
case "$MARIADB_PASSWORD:$MARIADB_ROOT_PASSWORD" in
|
||||||
|
*replace-with*)
|
||||||
|
echo "MariaDB placeholder passwords are forbidden in production." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "${#MARIADB_PASSWORD}" -lt 16 ] || [ "${#MARIADB_ROOT_PASSWORD}" -lt 16 ]; then
|
||||||
|
echo "MariaDB passwords must contain at least 16 characters in production." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WP_ENVIRONMENT_TYPE must be local, development, staging, or production." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exec docker-entrypoint.sh "$@"
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
volumes:
|
||||||
|
- test_db_data:/var/lib/mysql
|
||||||
|
|
||||||
|
wordpress:
|
||||||
|
volumes:
|
||||||
|
- test_wordpress_data:/var/www/html
|
||||||
|
|
||||||
|
wp-cli:
|
||||||
|
environment:
|
||||||
|
WP_URL: http://azionelab.org
|
||||||
|
WP_ADMIN_USER: test-admin
|
||||||
|
WP_ADMIN_PASSWORD: test-only-password-not-for-production
|
||||||
|
WP_ADMIN_EMAIL: test@example.org
|
||||||
|
volumes:
|
||||||
|
- test_wordpress_data:/var/www/html
|
||||||
|
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
aliases:
|
||||||
|
- azionelab.org
|
||||||
|
|
||||||
|
functional-tests:
|
||||||
|
build:
|
||||||
|
context: ./tests/functional
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
BASE_URL: http://azionelab.org
|
||||||
|
INVALID_HOST_URL: http://proxy
|
||||||
|
CI: "true"
|
||||||
|
depends_on:
|
||||||
|
proxy:
|
||||||
|
condition: service_healthy
|
||||||
|
profiles: ["test"]
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
shm_size: 1gb
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
pids_limit: 200
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
test_db_data:
|
||||||
|
test_wordpress_data:
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:11.8.8-noble
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: ${MARIADB_DATABASE:-azionelab}
|
||||||
|
MARIADB_USER: ${MARIADB_USER:-azionelab}
|
||||||
|
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-replace-with-a-local-password}
|
||||||
|
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-replace-with-a-local-root-password}
|
||||||
|
WP_ENVIRONMENT_TYPE: ${WP_ENVIRONMENT_TYPE:-local}
|
||||||
|
entrypoint: ["/bin/sh", "/usr/local/bin/check-environment.sh"]
|
||||||
|
command: ["mariadbd"]
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
- ./db/check-environment.sh:/usr/local/bin/check-environment.sh:ro
|
||||||
|
networks:
|
||||||
|
- data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 10s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
pids_limit: 300
|
||||||
|
|
||||||
|
wordpress:
|
||||||
|
build:
|
||||||
|
context: ./wordpress
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db:3306
|
||||||
|
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-azionelab}
|
||||||
|
WORDPRESS_DB_USER: ${MARIADB_USER:-azionelab}
|
||||||
|
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:-replace-with-a-local-password}
|
||||||
|
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-azl_}
|
||||||
|
WORDPRESS_DEBUG: ${WORDPRESS_DEBUG:-0}
|
||||||
|
WP_ENVIRONMENT_TYPE: ${WP_ENVIRONMENT_TYPE:-local}
|
||||||
|
WP_URL: ${WP_URL:-http://azionelab.org:8080}
|
||||||
|
WORDPRESS_CONFIG_EXTRA: |
|
||||||
|
if (!isset($$_SERVER['HTTP_HOST'])) {
|
||||||
|
$$_SERVER['HTTP_HOST'] = parse_url(getenv('WP_URL') ?: 'http://localhost', PHP_URL_HOST);
|
||||||
|
}
|
||||||
|
if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
||||||
|
$$_SERVER['HTTPS'] = 'on';
|
||||||
|
}
|
||||||
|
define('DISALLOW_FILE_EDIT', true);
|
||||||
|
define('WP_AUTO_UPDATE_CORE', 'minor');
|
||||||
|
define('WP_POST_REVISIONS', 10);
|
||||||
|
if (getenv('WP_ENVIRONMENT_TYPE') === 'production') {
|
||||||
|
define('FORCE_SSL_ADMIN', true);
|
||||||
|
define('DISALLOW_FILE_MODS', true);
|
||||||
|
}
|
||||||
|
volumes:
|
||||||
|
- wordpress_data:/var/www/html
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- data
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "php", "-r", "$$c=@file_get_contents('http://127.0.0.1/wp-login.php'); exit($$c===false ? 1 : 0);"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 60s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
pids_limit: 300
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
build:
|
||||||
|
context: ./nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
environment:
|
||||||
|
LETSENCRYPT_ENABLED: ${LETSENCRYPT_ENABLED:-0}
|
||||||
|
LETSENCRYPT_DOMAIN: ${LETSENCRYPT_DOMAIN:-azionelab.org}
|
||||||
|
TLS_RELOAD_INTERVAL_SECONDS: ${TLS_RELOAD_INTERVAL_SECONDS:-30}
|
||||||
|
TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-0}
|
||||||
|
volumes:
|
||||||
|
- letsencrypt_data:/etc/letsencrypt:ro
|
||||||
|
- certbot_challenges:/var/www/certbot:ro
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
ports:
|
||||||
|
- "${NGINX_BIND_ADDRESS:-127.0.0.1}:${NGINX_HTTP_PORT:-8080}:80"
|
||||||
|
- "${NGINX_BIND_ADDRESS:-127.0.0.1}:${NGINX_HTTPS_PORT:-8443}:443"
|
||||||
|
depends_on:
|
||||||
|
wordpress:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/nginx-health && wget -q -O /dev/null http://127.0.0.1/wordpress-health || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
start_period: 15s
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
pids_limit: 200
|
||||||
|
|
||||||
|
certbot:
|
||||||
|
image: certbot/certbot:v5.6.0
|
||||||
|
restart: unless-stopped
|
||||||
|
init: true
|
||||||
|
read_only: true
|
||||||
|
environment:
|
||||||
|
LETSENCRYPT_DOMAIN: ${LETSENCRYPT_DOMAIN:-azionelab.org}
|
||||||
|
LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL:-}
|
||||||
|
LETSENCRYPT_STAGING: ${LETSENCRYPT_STAGING:-1}
|
||||||
|
LETSENCRYPT_RENEW_INTERVAL_SECONDS: ${LETSENCRYPT_RENEW_INTERVAL_SECONDS:-43200}
|
||||||
|
LETSENCRYPT_RETRY_SECONDS: ${LETSENCRYPT_RETRY_SECONDS:-300}
|
||||||
|
entrypoint: ["/bin/sh", "/opt/certbot/renew.sh"]
|
||||||
|
volumes:
|
||||||
|
- ./certbot/renew.sh:/opt/certbot/renew.sh:ro
|
||||||
|
- letsencrypt_data:/etc/letsencrypt
|
||||||
|
- certbot_challenges:/var/www/certbot
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /var/lib/letsencrypt
|
||||||
|
- /var/log/letsencrypt
|
||||||
|
depends_on:
|
||||||
|
proxy:
|
||||||
|
condition: service_healthy
|
||||||
|
deploy:
|
||||||
|
replicas: ${LETSENCRYPT_ENABLED:-0}
|
||||||
|
networks:
|
||||||
|
- acme
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
pids_limit: 100
|
||||||
|
|
||||||
|
wp-cli:
|
||||||
|
image: wordpress:cli-2.12.0-php8.3
|
||||||
|
init: true
|
||||||
|
user: "33:33"
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: db:3306
|
||||||
|
WORDPRESS_DB_NAME: ${MARIADB_DATABASE:-azionelab}
|
||||||
|
WORDPRESS_DB_USER: ${MARIADB_USER:-azionelab}
|
||||||
|
WORDPRESS_DB_PASSWORD: ${MARIADB_PASSWORD:-replace-with-a-local-password}
|
||||||
|
WORDPRESS_TABLE_PREFIX: ${WORDPRESS_TABLE_PREFIX:-azl_}
|
||||||
|
WP_URL: ${WP_URL:-http://azionelab.org:8080}
|
||||||
|
WP_TITLE: ${WP_TITLE:-Azione!Lab}
|
||||||
|
WP_ADMIN_USER: ${WP_ADMIN_USER:-azionelab-admin}
|
||||||
|
WP_ADMIN_PASSWORD: ${WP_ADMIN_PASSWORD:-replace-with-a-local-admin-password}
|
||||||
|
WP_ADMIN_EMAIL: ${WP_ADMIN_EMAIL:-admin@example.org}
|
||||||
|
WP_ENVIRONMENT_TYPE: ${WP_ENVIRONMENT_TYPE:-local}
|
||||||
|
HOME: /tmp
|
||||||
|
HTTP_HOST: ${LETSENCRYPT_DOMAIN:-azionelab.org}
|
||||||
|
entrypoint: ["/bin/sh"]
|
||||||
|
command: ["/scripts/bootstrap.sh"]
|
||||||
|
volumes:
|
||||||
|
- wordpress_data:/var/www/html
|
||||||
|
- ./wp-cli/bootstrap.sh:/scripts/bootstrap.sh:ro
|
||||||
|
networks:
|
||||||
|
- data
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
wordpress:
|
||||||
|
condition: service_healthy
|
||||||
|
profiles: ["tools"]
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
pids_limit: 100
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
wordpress_data:
|
||||||
|
letsencrypt_data:
|
||||||
|
certbot_challenges:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
data:
|
||||||
|
internal: true
|
||||||
|
acme:
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# ADR-0001: WordPress single-page architecture
|
||||||
|
|
||||||
|
Date: 2026-06-24
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Azione!Lab needs a warm editorial single page whose primary content can be edited by
|
||||||
|
non-developers. This branch explicitly explores WordPress as an alternative product
|
||||||
|
architecture and starts independently from `main`.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use pinned WordPress 7.0/PHP 8.3 with MariaDB 11.8 LTS. Build a dependency-free custom
|
||||||
|
classic theme for exact semantic markup and responsive design. Store homepage fields as
|
||||||
|
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
|
||||||
|
post types so structured content is not lost when changing themes.
|
||||||
|
|
||||||
|
NGINX is the only public entry point. WP-CLI provides an idempotent opt-in bootstrap;
|
||||||
|
Certbot provides opt-in direct TLS. Docker networks isolate the database and functional
|
||||||
|
tests use separate volumes.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Editors use familiar WordPress screens without a page builder or required plugin.
|
||||||
|
- WordPress renders the page directly; there is no headless API/frontend deployment.
|
||||||
|
- WordPress and MariaDB replace Astro, Wagtail, Django, and PostgreSQL in this branch.
|
||||||
|
- Core/theme/plugin patches require an image rebuild in production.
|
||||||
|
- Fixed participation cards remain code until editorial requirements justify another
|
||||||
|
structured content type.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- A block theme was not selected because the first version prioritizes controlled
|
||||||
|
markup and a compact implementation over full-site editing.
|
||||||
|
- A commercial/custom-fields plugin was rejected as an unnecessary dependency.
|
||||||
|
- A headless WordPress frontend was rejected because it would recreate the operational
|
||||||
|
complexity this variant is intended to remove.
|
||||||
|
|
||||||
|
## Security impact
|
||||||
|
|
||||||
|
The architecture inherits WordPress's public CMS attack surface. The implementation
|
||||||
|
reduces it with network isolation, no direct application port, disabled file editing
|
||||||
|
and XML-RPC, production immutability, sanitization/escaping, rate limiting, security
|
||||||
|
headers, and fail-closed TLS. Admin MFA and allowlisting remain external controls.
|
||||||
|
|
||||||
|
## Operational impact
|
||||||
|
|
||||||
|
Database and WordPress files are coordinated state and must be backed up together.
|
||||||
|
Operators must monitor upstream security releases and rebuild pinned images. SMTP and
|
||||||
|
off-host storage are not part of this minimal base.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Revert the implementation commit and rebuild. Preserve WordPress, database, and
|
||||||
|
certificate volumes unless data deletion is intentional. Restore data only from a
|
||||||
|
verified coordinated backup.
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
Describe the project architecture here.
|
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
||||||
|
official WordPress 7.0/PHP 8.3 Apache image over the private `web` network. WordPress
|
||||||
|
connects to MariaDB 11.8 LTS over a separate internal `data` network. Neither WordPress
|
||||||
|
nor MariaDB publishes a host port.
|
||||||
|
|
||||||
Include:
|
The custom `azionelab` classic theme renders the public single page. Theme modifications
|
||||||
|
store the hero, manifesto, laboratory, teacher, lesson, and contact fields. The
|
||||||
|
`azionelab-content` must-use plugin registers Shows and Gallery custom post types so
|
||||||
|
structured editorial content survives a theme change. Images use WordPress featured
|
||||||
|
images with local SVG fallbacks.
|
||||||
|
|
||||||
- main components;
|
WP-CLI is an opt-in tools-profile service. Its idempotent bootstrap installs WordPress,
|
||||||
- runtime dependencies;
|
activates the theme, configures the site, and creates realistic demo content. Certbot is
|
||||||
- data flow;
|
another optional service, enabled only for direct deployments. It shares challenge and
|
||||||
- persistence;
|
certificate volumes with NGINX but has no container-control access.
|
||||||
- external integrations;
|
|
||||||
- deployment topology;
|
Persistent state lives in `db_data`, `wordpress_data`, and the certificate volumes.
|
||||||
- relevant ADRs.
|
Functional tests replace the first two with isolated test volumes and reach NGINX via
|
||||||
|
an internal `azionelab.org` network alias.
|
||||||
|
|
||||||
|
See [ADR-0001](adr/0001-wordpress-single-page.md).
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
Describe how this project is deployed.
|
## Local
|
||||||
|
|
||||||
Include:
|
Copy `.env.example` to `.env`, add `127.0.0.1 azionelab.org` to the hosts file, then:
|
||||||
|
|
||||||
- environments;
|
```bash
|
||||||
- Docker/Compose usage;
|
docker compose up --build -d
|
||||||
- required configuration;
|
docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||||
- secrets handling;
|
```
|
||||||
- exposed ports;
|
|
||||||
- volumes;
|
NGINX binds to loopback ports 8080/8443. WordPress and MariaDB remain private. The
|
||||||
- networks;
|
bootstrap is safe to rerun and does not duplicate demo records.
|
||||||
- deployment commands;
|
|
||||||
- rollback procedure.
|
## Production
|
||||||
|
|
||||||
|
Required controls:
|
||||||
|
|
||||||
|
- `WP_ENVIRONMENT_TYPE=production`;
|
||||||
|
- unique non-placeholder MariaDB passwords of at least 16 characters;
|
||||||
|
- `WORDPRESS_DEBUG=0` and a strong administrator password;
|
||||||
|
- an HTTPS `WP_URL`;
|
||||||
|
- managed secrets outside the Compose file;
|
||||||
|
- either direct Let's Encrypt termination or a trusted external load balancer;
|
||||||
|
- off-host database/file backups and monitoring.
|
||||||
|
|
||||||
|
When a load balancer terminates TLS, Certbot stays disabled. `TRUST_PROXY_HEADERS=1`
|
||||||
|
is safe only when firewall/network policy makes the load balancer the sole NGINX
|
||||||
|
caller and it overwrites `X-Forwarded-*` headers.
|
||||||
|
|
||||||
|
For direct TLS, public DNS and inbound ports 80/443 must reach NGINX. Start with
|
||||||
|
`LETSENCRYPT_STAGING=1`; application HTTP returns 503 until a certificate exists, then
|
||||||
|
redirects to HTTPS. Switch to the production CA only after validating DNS and firewall
|
||||||
|
behavior, removing only the staging certificate volume when necessary.
|
||||||
|
|
||||||
|
## State and rollback
|
||||||
|
|
||||||
|
Database and WordPress file volumes must be backed up together. Code rollback is a
|
||||||
|
container rebuild from a prior commit and does not require deleting volumes. Never use
|
||||||
|
`docker compose down --volumes` where content must survive. Test database restoration
|
||||||
|
in a disposable environment before any production restore.
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
# Operations
|
# Operations
|
||||||
|
|
||||||
Describe operational procedures.
|
## Routine commands
|
||||||
|
|
||||||
Include:
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
docker compose ps
|
||||||
|
docker compose logs -f proxy wordpress db certbot
|
||||||
|
docker compose --profile tools run --rm wp-cli -c 'wp core version'
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
- startup and shutdown;
|
The proxy healthcheck reaches WordPress, and WordPress health reaches its login route.
|
||||||
- health checks;
|
MariaDB uses its official readiness check. WordPress access logs are disabled while
|
||||||
- logs;
|
PHP warnings/errors remain visible. NGINX is the request audit trail; apply an explicit
|
||||||
- monitoring;
|
retention policy because it contains client metadata.
|
||||||
- backup and restore;
|
|
||||||
- routine maintenance;
|
## Updates
|
||||||
- known operational risks.
|
|
||||||
|
WordPress, PHP, MariaDB, NGINX, Certbot, and Playwright use explicit image versions.
|
||||||
|
Review security releases routinely, update pins in a task branch, rebuild, run the full
|
||||||
|
test suite, and deploy. Production disables WordPress web-based file modifications, so
|
||||||
|
image rebuilds are the update path.
|
||||||
|
|
||||||
|
## Backup and restore
|
||||||
|
|
||||||
|
Create database and `wordpress_data` backups in one maintenance window. Backups contain
|
||||||
|
credentials, accounts, contact information, and uploaded media; encrypt them, restrict
|
||||||
|
access, set retention, and store copies off-host.
|
||||||
|
|
||||||
|
A restore is destructive. Validate it on isolated volumes, then stop WordPress, restore
|
||||||
|
the database and file volume together, restart, and verify the homepage, media,
|
||||||
|
`/wp-admin/`, and user accounts.
|
||||||
|
|
||||||
|
## Known risks
|
||||||
|
|
||||||
|
- Local named volumes are not backups.
|
||||||
|
- SMTP is not configured; WordPress password-reset email needs an external mail service.
|
||||||
|
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
|
||||||
|
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
||||||
|
necessary plugins.
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
# Runbook
|
# Runbook
|
||||||
|
|
||||||
Operational runbook for this project.
|
## Installation screen remains visible
|
||||||
|
|
||||||
## Common tasks
|
1. Check `docker compose ps` for healthy database and WordPress services.
|
||||||
|
2. Run `docker compose --profile tools run --rm wp-cli /scripts/bootstrap.sh`.
|
||||||
|
3. Inspect `docker compose logs wordpress db` without printing secret values.
|
||||||
|
|
||||||
Document routine operational tasks here.
|
## NGINX returns 502
|
||||||
|
|
||||||
## Troubleshooting
|
1. Run `docker compose ps` and `docker compose exec proxy nginx -t`.
|
||||||
|
2. Check the WordPress health status and `docker compose logs proxy wordpress`.
|
||||||
|
3. Confirm the request host is exactly `azionelab.org`; unknown hosts return 404.
|
||||||
|
|
||||||
Document known issues, symptoms, checks, and remediation steps.
|
## Images or theme are missing
|
||||||
|
|
||||||
|
1. Confirm `wordpress_data` is mounted in both WordPress and WP-CLI.
|
||||||
|
2. Run `docker compose --profile tools run --rm wp-cli -c 'wp theme status azionelab'`.
|
||||||
|
3. Verify file ownership before changing permissions; never make the tree world-writable.
|
||||||
|
|
||||||
|
## Certificate issuance fails
|
||||||
|
|
||||||
|
Verify public DNS, inbound port 80, the operator email, and staging mode. Request a
|
||||||
|
missing `/.well-known/acme-challenge/` path: an NGINX 404 confirms the route is yours.
|
||||||
|
Avoid repeated production-CA retries while debugging.
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
Document rollback procedures here.
|
Revert the deployment commit and rebuild while preserving all named volumes. Restore
|
||||||
|
database/files only for a data rollback and only from a verified coordinated backup.
|
||||||
## Emergency contacts
|
|
||||||
|
|
||||||
Document project-specific escalation paths if appropriate.
|
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
# Security
|
# Security
|
||||||
|
|
||||||
Describe security assumptions and controls.
|
- NGINX is the only published service. MariaDB is restricted to an internal network;
|
||||||
|
WordPress is reachable only from NGINX and the database network.
|
||||||
|
- Production startup rejects placeholder or short database passwords. `.env` is
|
||||||
|
ignored and real secrets must come from the deployment secret manager.
|
||||||
|
- WordPress uses its normal capability, nonce, authentication, cookie, and password
|
||||||
|
controls. Theme settings sanitize input and templates escape output.
|
||||||
|
- File editing is always disabled. Production also disables web-based core, theme, and
|
||||||
|
plugin changes; patched images are rebuilt and redeployed instead.
|
||||||
|
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
||||||
|
and direct `wp-config.php` requests, and rate-limits login/public requests. Public
|
||||||
|
REST user enumeration and author archives are disabled.
|
||||||
|
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
||||||
|
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
||||||
|
requires inline style/script CSP allowances; do not treat this CSP as an XSS sanitizer.
|
||||||
|
- Forwarded client/protocol headers are overwritten by default. Trust mode is permitted
|
||||||
|
only behind a restricted load balancer that overwrites those headers.
|
||||||
|
- Direct TLS is fail-closed until a certificate exists. Certbot has a read-only root,
|
||||||
|
no Docker socket, dropped capabilities, and narrowly scoped volumes.
|
||||||
|
- Containers are not privileged and do not use host networking or the Docker socket.
|
||||||
|
WordPress/Apache retains the capabilities needed by the official image internally,
|
||||||
|
but no WordPress port is published.
|
||||||
|
- WordPress/Apache access logs are disabled to avoid duplicate client metadata and
|
||||||
|
healthcheck noise; NGINX remains the single request log and PHP warnings/errors stay
|
||||||
|
visible.
|
||||||
|
- Admin MFA, an IP/VPN allowlist, SMTP, malware scanning, log aggregation, and a secret
|
||||||
|
manager remain production operator responsibilities.
|
||||||
|
- Backups and uploads can contain personal data. Encrypt, restrict, retain, and delete
|
||||||
|
them according to the applicable privacy policy.
|
||||||
|
|
||||||
Include:
|
See [ADR-0001](adr/0001-wordpress-single-page.md).
|
||||||
|
|
||||||
- authentication;
|
|
||||||
- authorization;
|
|
||||||
- network exposure;
|
|
||||||
- TLS/certificates;
|
|
||||||
- secrets management;
|
|
||||||
- logging of sensitive data;
|
|
||||||
- container privileges;
|
|
||||||
- filesystem permissions;
|
|
||||||
- dependency management;
|
|
||||||
- relevant ADRs.
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
Describe how tests are executed.
|
All checks run inside Docker containers.
|
||||||
|
|
||||||
All tests should run inside Docker containers.
|
|
||||||
|
|
||||||
## Canonical test command
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CHANGE_ME
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/functions.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/theme/front-page.php
|
||||||
|
docker compose run --rm --no-deps wordpress php -l /opt/azionelab/azionelab-content.php
|
||||||
|
docker compose run --rm --no-deps proxy nginx -t
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build -d db wordpress proxy
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile tools run --rm wp-cli /scripts/bootstrap.sh
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.test.yml --profile test run --build --rm functional-tests
|
||||||
|
docker compose config --quiet
|
||||||
|
LETSENCRYPT_ENABLED=1 docker compose config --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test categories
|
The override replaces normal database and WordPress volumes, maps `azionelab.org` to
|
||||||
|
the internal proxy, and never publishes an extra port. Browser tests cover content and
|
||||||
|
section order, contact actions, mobile overflow/navigation, semantic landmarks, image
|
||||||
|
alternatives, admin routing, security headers, blocked sensitive routes, and unknown
|
||||||
|
virtual hosts.
|
||||||
|
|
||||||
Describe applicable categories:
|
Subjective visual review and a real-device accessibility audit remain manual release
|
||||||
|
checks.
|
||||||
- unit tests;
|
|
||||||
- integration tests;
|
|
||||||
- linting;
|
|
||||||
- formatting checks;
|
|
||||||
- Ansible syntax checks;
|
|
||||||
- Docker/Compose validation;
|
|
||||||
- smoke tests.
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
enabled="${LETSENCRYPT_ENABLED:-0}"
|
||||||
|
domain="${LETSENCRYPT_DOMAIN:-azionelab.org}"
|
||||||
|
interval="${TLS_RELOAD_INTERVAL_SECONDS:-30}"
|
||||||
|
trust_proxy_headers="${TRUST_PROXY_HEADERS:-0}"
|
||||||
|
certificate="/etc/letsencrypt/live/${domain}/fullchain.pem"
|
||||||
|
private_key="/etc/letsencrypt/live/${domain}/privkey.pem"
|
||||||
|
http_config="/etc/nginx/http-enabled/site.conf"
|
||||||
|
tls_config="/etc/nginx/tls-enabled/site.conf"
|
||||||
|
tls_template="/etc/nginx/templates/tls.conf.template.source"
|
||||||
|
base_template="/etc/nginx/templates/default.conf.template.source"
|
||||||
|
base_config="/etc/nginx/conf.d/default.conf"
|
||||||
|
proxy_template="/etc/nginx/templates/proxy-routes.conf.template.source"
|
||||||
|
proxy_routes="/etc/nginx/snippets/proxy-routes.conf"
|
||||||
|
|
||||||
|
case "$domain" in
|
||||||
|
"" | *[!A-Za-z0-9.-]* | .* | *. | *..* | -* | *- | *.-* | *-.*)
|
||||||
|
echo "Invalid LETSENCRYPT_DOMAIN: $domain" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case "$domain" in *.*) ;; *) echo "LETSENCRYPT_DOMAIN must be a fully qualified domain name." >&2; exit 1 ;; esac
|
||||||
|
case "$enabled" in 0 | 1) ;; *) echo "LETSENCRYPT_ENABLED must be 0 or 1." >&2; exit 1 ;; esac
|
||||||
|
case "$trust_proxy_headers" in
|
||||||
|
0) forwarded_for='$remote_addr'; forwarded_proto='$scheme' ;;
|
||||||
|
1) forwarded_for='$proxy_add_x_forwarded_for'; forwarded_proto='$effective_forwarded_proto' ;;
|
||||||
|
*) echo "TRUST_PROXY_HEADERS must be 0 or 1." >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$interval" in "" | *[!0-9]*) echo "TLS_RELOAD_INTERVAL_SECONDS must be a positive integer." >&2; exit 1 ;; esac
|
||||||
|
[ "$interval" -gt 0 ] || { echo "TLS_RELOAD_INTERVAL_SECONDS must be positive." >&2; exit 1; }
|
||||||
|
|
||||||
|
certificate_state() {
|
||||||
|
if [ ! -s "$certificate" ] || [ ! -s "$private_key" ]; then
|
||||||
|
echo missing
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
cksum "$certificate" "$private_key" | cksum | awk '{print $1 ":" $2}'
|
||||||
|
}
|
||||||
|
|
||||||
|
render_http() {
|
||||||
|
printf 'include %s;\n' "$proxy_routes" > "$http_config"
|
||||||
|
rm -f "$tls_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
render_pending() {
|
||||||
|
cat > "$http_config" <<'EOF'
|
||||||
|
location / {
|
||||||
|
add_header Retry-After "60" always;
|
||||||
|
return 503;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
rm -f "$tls_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
render_https() {
|
||||||
|
sed "s|__DOMAIN__|${domain}|g" "$tls_template" > "${tls_config}.tmp"
|
||||||
|
mv "${tls_config}.tmp" "$tls_config"
|
||||||
|
printf 'location ^~ / { return 301 https://$host$request_uri; }\n' > "$http_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
sed -e "s|__X_FORWARDED_FOR__|${forwarded_for}|g" -e "s|__X_FORWARDED_PROTO__|${forwarded_proto}|g" "$proxy_template" > "${proxy_routes}.tmp"
|
||||||
|
mv "${proxy_routes}.tmp" "$proxy_routes"
|
||||||
|
sed "s|__DOMAIN__|${domain}|g" "$base_template" > "${base_config}.tmp"
|
||||||
|
mv "${base_config}.tmp" "$base_config"
|
||||||
|
|
||||||
|
if [ "$enabled" = 0 ]; then
|
||||||
|
render_http
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
state="$(certificate_state)"
|
||||||
|
if [ "$state" = missing ]; then render_pending; else render_https; fi
|
||||||
|
|
||||||
|
(
|
||||||
|
while sleep "$interval"; do
|
||||||
|
next_state="$(certificate_state)"
|
||||||
|
[ "$next_state" = "$state" ] && continue
|
||||||
|
if [ "$next_state" = missing ]; then render_pending; else render_https; fi
|
||||||
|
if nginx -t; then
|
||||||
|
nginx -s reload
|
||||||
|
state="$next_state"
|
||||||
|
else
|
||||||
|
echo "TLS configuration reload failed; retrying after ${interval}s." >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
) &
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
FROM nginx:1.30.2-alpine
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/templates/default.conf.template.source
|
||||||
|
COPY proxy-routes.conf /etc/nginx/templates/proxy-routes.conf.template.source
|
||||||
|
COPY tls.conf.template /etc/nginx/templates/tls.conf.template.source
|
||||||
|
COPY 40-configure-tls.sh /docker-entrypoint.d/40-configure-tls.sh
|
||||||
|
|
||||||
|
RUN chmod 755 /docker-entrypoint.d/40-configure-tls.sh \
|
||||||
|
&& rm -f /etc/nginx/conf.d/default.conf \
|
||||||
|
&& mkdir -p /etc/nginx/http-enabled /etc/nginx/tls-enabled /etc/nginx/snippets /var/www/certbot
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
map $http_x_forwarded_proto $effective_forwarded_proto {
|
||||||
|
default $scheme;
|
||||||
|
https https;
|
||||||
|
}
|
||||||
|
|
||||||
|
server_tokens off;
|
||||||
|
limit_req_status 429;
|
||||||
|
limit_req_zone $binary_remote_addr zone=site:10m rate=30r/s;
|
||||||
|
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
|
||||||
|
|
||||||
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||||
|
|
||||||
|
upstream wordpress_backend {
|
||||||
|
zone wordpress_backend 64k;
|
||||||
|
server wordpress:80 resolve;
|
||||||
|
keepalive 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location = /nginx-health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /wordpress-health {
|
||||||
|
access_log off;
|
||||||
|
allow 127.0.0.1;
|
||||||
|
deny all;
|
||||||
|
proxy_set_header Host __DOMAIN__;
|
||||||
|
proxy_pass http://wordpress_backend/wp-login.php;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name __DOMAIN__;
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
default_type text/plain;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/nginx/http-enabled/site.conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/nginx/tls-enabled/*.conf;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
client_max_body_size 16m;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For __X_FORWARDED_FOR__;
|
||||||
|
proxy_set_header X-Forwarded-Proto __X_FORWARDED_PROTO__;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=(), usb=()" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
|
||||||
|
location = /xmlrpc.php {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /wp-login.php {
|
||||||
|
limit_req zone=login burst=5 nodelay;
|
||||||
|
proxy_pass http://wordpress_backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /wp-admin/ {
|
||||||
|
limit_req zone=site burst=60 nodelay;
|
||||||
|
proxy_pass http://wordpress_backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^/(?:wp-content/uploads|wp-content/files)/.*\.php$ {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ (^|/)\. {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /wp-config.php {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
limit_req zone=site burst=60 nodelay;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; img-src 'self' data: https:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=(), usb=()" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
proxy_pass http://wordpress_backend;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 443 ssl default_server;
|
||||||
|
http2 on;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/__DOMAIN__/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/__DOMAIN__/privkey.pem;
|
||||||
|
ssl_reject_handshake on;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name __DOMAIN__;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/__DOMAIN__/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/__DOMAIN__/privkey.pem;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=15552000" always;
|
||||||
|
|
||||||
|
include /etc/nginx/snippets/proxy-routes.conf;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright:v1.61.1-noble
|
||||||
|
|
||||||
|
WORKDIR /tests
|
||||||
|
RUN chown pwuser:pwuser /tests
|
||||||
|
|
||||||
|
COPY --chown=pwuser:pwuser package.json package-lock.json ./
|
||||||
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY --chown=pwuser:pwuser . .
|
||||||
|
|
||||||
|
USER pwuser
|
||||||
|
CMD ["npm", "test"]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "azionelab-wordpress-functional-tests",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "azionelab-wordpress-functional-tests",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.61.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "azionelab-wordpress-functional-tests",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"test": "playwright test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.61.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
timeout: 30_000,
|
||||||
|
expect: { timeout: 5_000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: true,
|
||||||
|
retries: 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: "line",
|
||||||
|
outputDir: "test-results",
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.BASE_URL ?? "http://azionelab.org",
|
||||||
|
browserName: "chromium",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
video: "off",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
const sectionIds = [
|
||||||
|
"inizio",
|
||||||
|
"laboratorio",
|
||||||
|
"maestro",
|
||||||
|
"lezioni",
|
||||||
|
"spettacoli",
|
||||||
|
"galleria",
|
||||||
|
"contatti",
|
||||||
|
];
|
||||||
|
|
||||||
|
test("renders the complete seeded single page", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/Azione!Lab/);
|
||||||
|
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
|
||||||
|
"Il teatro diventa presenza.",
|
||||||
|
);
|
||||||
|
await expect(page.getByText("Ernesto Estatico")).toBeVisible();
|
||||||
|
await expect(page.getByText(/Via dell'Epomeo 9999, Napoli/).first()).toBeVisible();
|
||||||
|
await expect(page.locator(".feature-card")).toHaveCount(3);
|
||||||
|
await expect(page.locator(".show-card")).toHaveCount(2);
|
||||||
|
await expect(page.locator(".gallery-item")).toHaveCount(4);
|
||||||
|
|
||||||
|
const positions = await page.evaluate((ids) => ids.map((id) => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (!element) throw new Error(`Missing section #${id}`);
|
||||||
|
return element.getBoundingClientRect().top + window.scrollY;
|
||||||
|
}), sectionIds);
|
||||||
|
expect(positions).toEqual([...positions].sort((a, b) => a - b));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provides usable contact actions", async ({ page }) => {
|
||||||
|
await page.goto("/#contatti");
|
||||||
|
|
||||||
|
await expect(page.getByRole("link", { name: /Scrivi una mail/ })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"mailto:ciao@azionelab.org",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("link", { name: /Chiama/ })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"tel:+393331234567",
|
||||||
|
);
|
||||||
|
await expect(page.getByRole("link", { name: /WhatsApp/ })).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"https://wa.me/393331234567",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("supports mobile navigation without horizontal overflow", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const menu = page.locator("details.mobile-menu");
|
||||||
|
await menu.locator("summary").click();
|
||||||
|
await expect(menu).toHaveAttribute("open", "");
|
||||||
|
await menu.getByRole("link", { name: "Le lezioni" }).click();
|
||||||
|
await expect(page).toHaveURL(/#lezioni$/);
|
||||||
|
|
||||||
|
const dimensions = await page.evaluate(() => ({
|
||||||
|
clientWidth: document.documentElement.clientWidth,
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
}));
|
||||||
|
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses semantic landmarks, image alternatives and keyboard access", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page.getByRole("banner")).toHaveCount(1);
|
||||||
|
await expect(page.getByRole("main")).toHaveCount(1);
|
||||||
|
await expect(page.getByRole("contentinfo")).toHaveCount(1);
|
||||||
|
await expect(page.getByRole("heading", { level: 1 })).toHaveCount(1);
|
||||||
|
const alternatives = await page.locator("img").evaluateAll((images) =>
|
||||||
|
images.map((image) => image.getAttribute("alt")?.trim() ?? ""),
|
||||||
|
);
|
||||||
|
expect(alternatives.length).toBeGreaterThan(0);
|
||||||
|
expect(alternatives.every(Boolean)).toBe(true);
|
||||||
|
|
||||||
|
const images = page.locator("img");
|
||||||
|
for (let index = 0; index < await images.count(); index += 1) {
|
||||||
|
const image = images.nth(index);
|
||||||
|
await image.scrollIntoViewIfNeeded();
|
||||||
|
await expect
|
||||||
|
.poll(() => image.evaluate((element) => element.naturalWidth))
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
await expect(page.getByRole("link", { name: "Vai al contenuto" })).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("protects the edge and exposes the WordPress admin", async ({ page, request, playwright }) => {
|
||||||
|
const response = await request.get("/");
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
expect(response.headers()["content-security-policy"]).toContain("object-src 'none'");
|
||||||
|
expect(response.headers()["x-content-type-options"]).toBe("nosniff");
|
||||||
|
expect(response.headers()["permissions-policy"]).toContain("camera=()");
|
||||||
|
expect((await request.get("/xmlrpc.php")).status()).toBe(403);
|
||||||
|
expect((await request.get("/.env")).status()).toBe(404);
|
||||||
|
expect((await request.get("/wp-config.php")).status()).toBe(404);
|
||||||
|
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
||||||
|
|
||||||
|
await page.goto("/wp-admin/");
|
||||||
|
await expect(page).toHaveURL(/\/wp-login\.php/);
|
||||||
|
await expect(page.locator('input[name="log"]')).toBeVisible();
|
||||||
|
await expect(page.locator('input[name="pwd"]')).toBeVisible();
|
||||||
|
|
||||||
|
const invalidHost = await playwright.request.newContext({
|
||||||
|
baseURL: process.env.INVALID_HOST_URL ?? "http://proxy",
|
||||||
|
});
|
||||||
|
expect((await invalidHost.get("/")).status()).toBe(404);
|
||||||
|
await invalidHost.dispose();
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# BEGIN WordPress
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
RewriteBase /
|
||||||
|
RewriteRule ^index\.php$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
# END WordPress
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM wordpress:7.0.0-php8.3-apache
|
||||||
|
|
||||||
|
COPY php.ini /usr/local/etc/php/conf.d/azionelab.ini
|
||||||
|
COPY .htaccess /opt/azionelab/.htaccess
|
||||||
|
COPY entrypoint-wrapper.sh /usr/local/bin/azionelab-entrypoint
|
||||||
|
COPY theme/azionelab /opt/azionelab/theme
|
||||||
|
COPY mu-plugins/azionelab-content.php /opt/azionelab/azionelab-content.php
|
||||||
|
|
||||||
|
RUN sed -ri 's!^[[:space:]]*CustomLog .*!CustomLog /dev/null combined!' /etc/apache2/sites-available/000-default.conf \
|
||||||
|
&& chmod 755 /usr/local/bin/azionelab-entrypoint \
|
||||||
|
&& chown -R www-data:www-data /opt/azionelab
|
||||||
|
|
||||||
|
ENTRYPOINT ["azionelab-entrypoint"]
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
environment="${WP_ENVIRONMENT_TYPE:-local}"
|
||||||
|
case "$environment" in
|
||||||
|
local | development | staging) ;;
|
||||||
|
production)
|
||||||
|
password="${WORDPRESS_DB_PASSWORD:-}"
|
||||||
|
case "$password" in
|
||||||
|
"" | *replace-with*)
|
||||||
|
echo "A non-placeholder WORDPRESS_DB_PASSWORD is required in production." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "${#password}" -lt 16 ]; then
|
||||||
|
echo "WORDPRESS_DB_PASSWORD must contain at least 16 characters in production." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "WP_ENVIRONMENT_TYPE must be local, development, staging, or production." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${1:-}" = "apache2-foreground" ]; then
|
||||||
|
mkdir -p /var/www/html/wp-content/themes /var/www/html/wp-content/mu-plugins
|
||||||
|
rm -rf /var/www/html/wp-content/themes/azionelab
|
||||||
|
cp -a /opt/azionelab/theme /var/www/html/wp-content/themes/azionelab
|
||||||
|
cp /opt/azionelab/azionelab-content.php /var/www/html/wp-content/mu-plugins/azionelab-content.php
|
||||||
|
if [ ! -e /var/www/html/.htaccess ]; then
|
||||||
|
cp /opt/azionelab/.htaccess /var/www/html/.htaccess
|
||||||
|
fi
|
||||||
|
chown -R www-data:www-data \
|
||||||
|
/var/www/html/wp-content/themes/azionelab \
|
||||||
|
/var/www/html/wp-content/mu-plugins/azionelab-content.php \
|
||||||
|
/var/www/html/.htaccess
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec docker-entrypoint.sh "$@"
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Azione!Lab structured content
|
||||||
|
* Description: Shows and gallery content types used by the Azione!Lab theme.
|
||||||
|
* Version: 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_register_content_types(): void {
|
||||||
|
register_post_type(
|
||||||
|
'azl_show',
|
||||||
|
array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => 'Spettacoli',
|
||||||
|
'singular_name' => 'Spettacolo',
|
||||||
|
'add_new_item' => 'Aggiungi spettacolo',
|
||||||
|
'edit_item' => 'Modifica spettacolo',
|
||||||
|
),
|
||||||
|
'public' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'menu_icon' => 'dashicons-tickets-alt',
|
||||||
|
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes' ),
|
||||||
|
'has_archive' => false,
|
||||||
|
'rewrite' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
register_post_type(
|
||||||
|
'azl_gallery',
|
||||||
|
array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => 'Galleria',
|
||||||
|
'singular_name' => 'Foto',
|
||||||
|
'add_new_item' => 'Aggiungi foto',
|
||||||
|
'edit_item' => 'Modifica foto',
|
||||||
|
),
|
||||||
|
'public' => false,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'menu_icon' => 'dashicons-format-gallery',
|
||||||
|
'supports' => array( 'title', 'thumbnail', 'page-attributes' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'init', 'azionelab_register_content_types' );
|
||||||
|
|
||||||
|
function azionelab_add_meta_boxes(): void {
|
||||||
|
add_meta_box( 'azl_show_details', 'Dettagli spettacolo', 'azionelab_show_meta_box', 'azl_show', 'normal' );
|
||||||
|
add_meta_box( 'azl_gallery_details', 'Dettagli foto', 'azionelab_gallery_meta_box', 'azl_gallery', 'normal' );
|
||||||
|
}
|
||||||
|
add_action( 'add_meta_boxes', 'azionelab_add_meta_boxes' );
|
||||||
|
|
||||||
|
function azionelab_show_meta_box( WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'azionelab_save_meta', 'azionelab_meta_nonce' );
|
||||||
|
$year = get_post_meta( $post->ID, 'azl_show_year', true );
|
||||||
|
$location = get_post_meta( $post->ID, 'azl_show_location', true );
|
||||||
|
?>
|
||||||
|
<p><label for="azl_show_year"><strong>Anno</strong></label><br><input id="azl_show_year" name="azl_show_year" type="number" min="1900" max="2100" value="<?php echo esc_attr( $year ); ?>"></p>
|
||||||
|
<p><label for="azl_show_location"><strong>Luogo</strong></label><br><input id="azl_show_location" name="azl_show_location" type="text" class="widefat" value="<?php echo esc_attr( $location ); ?>"></p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_gallery_meta_box( WP_Post $post ): void {
|
||||||
|
wp_nonce_field( 'azionelab_save_meta', 'azionelab_meta_nonce' );
|
||||||
|
$category = get_post_meta( $post->ID, 'azl_gallery_category', true );
|
||||||
|
?>
|
||||||
|
<p><label for="azl_gallery_category"><strong>Categoria</strong></label><br>
|
||||||
|
<select id="azl_gallery_category" name="azl_gallery_category">
|
||||||
|
<?php foreach ( array( 'Lezioni', 'Backstage', 'Spettacoli', 'Gruppo' ) as $option ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $option ); ?>" <?php selected( $category, $option ); ?>><?php echo esc_html( $option ); ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_save_meta( int $post_id ): void {
|
||||||
|
if ( ! isset( $_POST['azionelab_meta_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['azionelab_meta_nonce'] ) ), 'azionelab_save_meta' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post_type = get_post_type( $post_id );
|
||||||
|
if ( 'azl_show' === $post_type ) {
|
||||||
|
$year = isset( $_POST['azl_show_year'] ) ? absint( $_POST['azl_show_year'] ) : 0;
|
||||||
|
$location = isset( $_POST['azl_show_location'] ) ? sanitize_text_field( wp_unslash( $_POST['azl_show_location'] ) ) : '';
|
||||||
|
update_post_meta( $post_id, 'azl_show_year', $year );
|
||||||
|
update_post_meta( $post_id, 'azl_show_location', $location );
|
||||||
|
} elseif ( 'azl_gallery' === $post_type ) {
|
||||||
|
$allowed = array( 'Lezioni', 'Backstage', 'Spettacoli', 'Gruppo' );
|
||||||
|
$category = isset( $_POST['azl_gallery_category'] ) ? sanitize_text_field( wp_unslash( $_POST['azl_gallery_category'] ) ) : '';
|
||||||
|
update_post_meta( $post_id, 'azl_gallery_category', in_array( $category, $allowed, true ) ? $category : 'Lezioni' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'save_post', 'azionelab_save_meta' );
|
||||||
|
|
||||||
|
add_filter( 'xmlrpc_enabled', '__return_false' );
|
||||||
|
add_filter( 'comments_open', '__return_false', 20 );
|
||||||
|
add_filter( 'pings_open', '__return_false', 20 );
|
||||||
|
remove_action( 'wp_head', 'wp_generator' );
|
||||||
|
add_filter( 'the_generator', '__return_empty_string' );
|
||||||
|
|
||||||
|
function azionelab_restrict_public_user_api( $result, WP_REST_Server $server, WP_REST_Request $request ) {
|
||||||
|
unset( $server );
|
||||||
|
if ( ! is_user_logged_in() && str_starts_with( $request->get_route(), '/wp/v2/users' ) ) {
|
||||||
|
return new WP_Error( 'rest_no_route', 'No route was found matching the URL and request method.', array( 'status' => 404 ) );
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
add_filter( 'rest_pre_dispatch', 'azionelab_restrict_public_user_api', 10, 3 );
|
||||||
|
|
||||||
|
function azionelab_disable_public_author_archives(): void {
|
||||||
|
if ( is_author() && ! is_user_logged_in() ) {
|
||||||
|
global $wp_query;
|
||||||
|
$wp_query->set_404();
|
||||||
|
status_header( 404 );
|
||||||
|
nocache_headers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'template_redirect', 'azionelab_disable_public_author_archives' );
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
expose_php = Off
|
||||||
|
display_errors = Off
|
||||||
|
log_errors = On
|
||||||
|
memory_limit = 256M
|
||||||
|
post_max_size = 16M
|
||||||
|
upload_max_filesize = 16M
|
||||||
|
max_execution_time = 60
|
||||||
|
session.cookie_httponly = 1
|
||||||
|
session.cookie_samesite = Lax
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 700" role="img" aria-labelledby="t"><title id="t">Ascolto e movimento</title><rect width="860" height="700" fill="#D8C7AE"/><circle cx="220" cy="220" r="88" fill="#FFF8EF"/><circle cx="638" cy="170" r="76" fill="#A4513B"/><path d="M62 700l101-378c83-24 151 26 202 151l-37 227z" fill="#515D53"/><path d="M487 700l75-408c81-21 151 47 207 202v206z" fill="#4A433A"/><path d="M304 420c122-83 232-112 331-86" fill="none" stroke="#B8674C" stroke-width="18" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 538 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 700" role="img" aria-labelledby="t"><title id="t">Dietro le quinte</title><rect width="860" height="700" fill="#4A433A"/><path d="M0 0h230v700H0zM630 0h230v700H630z" fill="#A4513B"/><ellipse cx="430" cy="720" rx="275" ry="346" fill="#D8C7AE"/><circle cx="430" cy="286" r="88" fill="#FFF8EF"/><path d="M290 700c24-192 71-298 140-318 82 25 129 131 140 318z" fill="#515D53"/></svg>
|
||||||
|
After Width: | Height: | Size: 436 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 700" role="img" aria-labelledby="t"><title id="t">Una scena dello spettacolo finale</title><rect width="860" height="700" fill="#515D53"/><circle cx="430" cy="350" r="251" fill="#B8674C"/><path d="M0 560c234-153 497-163 860-41v181H0z" fill="#D8C7AE"/><path d="M157 560c33-136 90-212 171-228 92 26 151 104 177 236zM496 550c11-121 58-202 142-244 93 43 140 128 141 254z" fill="#4A433A"/><circle cx="326" cy="270" r="52" fill="#FFF8EF"/><circle cx="639" cy="251" r="52" fill="#A4513B"/></svg>
|
||||||
|
After Width: | Height: | Size: 546 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 700" role="img" aria-labelledby="t"><title id="t">Il gruppo del laboratorio</title><rect width="860" height="700" fill="#FFF8EF"/><circle cx="430" cy="350" r="292" fill="#D8C7AE"/><circle cx="229" cy="250" r="58" fill="#A4513B"/><circle cx="430" cy="188" r="58" fill="#515D53"/><circle cx="631" cy="250" r="58" fill="#4A433A"/><path d="M111 615c15-171 54-259 117-265 73 4 116 93 130 265z" fill="#4A433A"/><path d="M313 615c13-203 52-304 117-305 75 2 115 104 120 305z" fill="#A4513B"/><path d="M514 615c17-171 56-259 117-265 73 4 116 93 130 265z" fill="#515D53"/></svg>
|
||||||
|
After Width: | Height: | Size: 626 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 1120" role="img" aria-labelledby="t d"><title id="t">Il gruppo in scena</title><desc id="d">Composizione astratta ispirata al movimento teatrale</desc><rect width="960" height="1120" fill="#D8C7AE"/><path d="M0 780c208-186 425-245 651-178 133 40 236 107 309 201v317H0z" fill="#515D53"/><circle cx="278" cy="390" r="118" fill="#FFF8EF"/><circle cx="642" cy="320" r="102" fill="#A4513B"/><path d="M137 845c32-266 80-410 145-431 91 17 157 164 198 441z" fill="#4A433A"/><path d="M487 855c38-300 90-461 157-482 96 30 156 191 179 482z" fill="#B8674C"/><path d="M315 525c117 95 219 100 306 15" fill="none" stroke="#F3EBDD" stroke-width="22" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 722 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 760" role="img" aria-labelledby="t"><title id="t">Esercizio nello spazio</title><rect width="960" height="760" fill="#FFF8EF"/><circle cx="480" cy="380" r="310" fill="#D8C7AE"/><circle cx="306" cy="250" r="72" fill="#A4513B"/><circle cx="670" cy="230" r="68" fill="#515D53"/><path d="M120 700c48-245 111-373 189-384 91 11 155 139 194 384z" fill="#4A433A"/><path d="M480 700c45-258 108-389 190-393 94 20 157 151 188 393z" fill="#B8674C"/><path d="M380 405c108-77 208-86 301-28" fill="none" stroke="#FFF8EF" stroke-width="18" stroke-linecap="round"/></svg>
|
||||||
|
After Width: | Height: | Size: 612 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 880" role="img" aria-labelledby="t"><title id="t">Le cose che restano</title><rect width="700" height="880" fill="#4A433A"/><circle cx="350" cy="350" r="245" fill="#D8C7AE"/><path d="M0 610c181-123 414-128 700-15v285H0z" fill="#515D53"/><path d="M214 682c25-186 70-286 136-301 75 21 122 122 140 301z" fill="#A4513B"/><text x="350" y="790" fill="#FFF8EF" text-anchor="middle" font-family="Georgia,serif" font-size="44">LE COSE CHE RESTANO</text></svg>
|
||||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 880" role="img" aria-labelledby="t"><title id="t">Fuori campo</title><rect width="700" height="880" fill="#D8C7AE"/><rect x="76" y="72" width="548" height="624" rx="274" fill="#A4513B"/><circle cx="350" cy="317" r="112" fill="#FFF8EF"/><path d="M145 696c35-223 105-344 205-363 116 24 184 145 205 363z" fill="#515D53"/><text x="350" y="790" fill="#1D1A17" text-anchor="middle" font-family="Georgia,serif" font-size="56">FUORI CAMPO</text></svg>
|
||||||
|
After Width: | Height: | Size: 501 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 900" role="img" aria-labelledby="t"><title id="t">Ritratto del maestro Ernesto Estatico</title><rect width="760" height="900" fill="#515D53"/><circle cx="380" cy="285" r="142" fill="#D8C7AE"/><path d="M92 900c24-321 120-491 288-511 173 24 269 194 288 511z" fill="#4A433A"/><path d="M258 297c67 42 139 42 215 0" fill="none" stroke="#A4513B" stroke-width="15" stroke-linecap="round"/><circle cx="330" cy="260" r="10" fill="#1D1A17"/><circle cx="430" cy="260" r="10" fill="#1D1A17"/><path d="M0 760c213-78 466-77 760 3v137H0z" fill="#A4513B" opacity=".78"/></svg>
|
||||||
|
After Width: | Height: | Size: 618 B |
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
/** Site footer. */
|
||||||
|
|
||||||
|
$footer_text = azionelab_mod( 'footer_text' );
|
||||||
|
$privacy_url = get_privacy_policy_url();
|
||||||
|
?>
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<p><strong><?php bloginfo( 'name' ); ?></strong> · <?php echo esc_html( $footer_text ); ?></p>
|
||||||
|
<p>
|
||||||
|
<?php if ( $privacy_url ) : ?>
|
||||||
|
<a href="<?php echo esc_url( $privacy_url ); ?>">Privacy</a>
|
||||||
|
<?php else : ?>
|
||||||
|
Privacy — TODO
|
||||||
|
<?php endif; ?>
|
||||||
|
· Tema Azione!Lab
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<?php wp_footer(); ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
/** Homepage single-page template. */
|
||||||
|
|
||||||
|
get_header();
|
||||||
|
|
||||||
|
$hero_image = get_theme_mod( 'hero_image' ) ?: azionelab_asset( 'hero.svg' );
|
||||||
|
$laboratory_image = get_theme_mod( 'laboratory_image' ) ?: azionelab_asset( 'laboratory.svg' );
|
||||||
|
$teacher_image = get_theme_mod( 'teacher_image' ) ?: azionelab_asset( 'teacher.svg' );
|
||||||
|
$features = array(
|
||||||
|
array( 'Per chi inizia', 'Non serve esperienza: serve curiosità, ascolto e voglia di mettersi in gioco.' ),
|
||||||
|
array( 'Per chi vuole crescere', 'Un tempo regolare per approfondire strumenti, consapevolezza e libertà espressiva.' ),
|
||||||
|
array( 'Per chi ama il gruppo', 'La scena nasce dalla fiducia: si crea insieme, rispettando tempi e sensibilità diverse.' ),
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<main id="contenuto">
|
||||||
|
<section class="hero section" id="inizio" aria-labelledby="hero-title">
|
||||||
|
<div class="container hero-grid">
|
||||||
|
<div class="hero-copy">
|
||||||
|
<p class="eyebrow">Laboratorio teatrale · corpo, voce, relazione</p>
|
||||||
|
<h1 id="hero-title"><?php echo esc_html( azionelab_mod( 'hero_title' ) ); ?></h1>
|
||||||
|
<p class="hero-subtitle"><?php echo esc_html( azionelab_mod( 'hero_subtitle' ) ); ?></p>
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="button" href="#contatti"><?php echo esc_html( azionelab_mod( 'hero_primary_label' ) ); ?></a>
|
||||||
|
<a class="text-link" href="#galleria"><?php echo esc_html( azionelab_mod( 'hero_secondary_label' ) ); ?> <span aria-hidden="true">↓</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figure class="hero-visual">
|
||||||
|
<img src="<?php echo esc_url( $hero_image ); ?>" alt="Il gruppo durante un esercizio teatrale" width="960" height="1120" fetchpriority="high">
|
||||||
|
<figcaption>Il gruppo è materia viva.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="manifesto section" aria-labelledby="manifesto-title">
|
||||||
|
<div class="container narrow">
|
||||||
|
<p class="eyebrow">Il nostro modo di lavorare</p>
|
||||||
|
<h2 id="manifesto-title"><?php echo esc_html( azionelab_mod( 'intro_title' ) ); ?></h2>
|
||||||
|
<div class="manifesto-text rich-text"><?php echo wp_kses_post( wpautop( azionelab_mod( 'intro_body' ) ) ); ?></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features section" aria-labelledby="features-title">
|
||||||
|
<div class="container">
|
||||||
|
<p class="eyebrow">Perché partecipare</p>
|
||||||
|
<h2 id="features-title">Un percorso aperto</h2>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<?php foreach ( $features as $index => $feature ) : ?>
|
||||||
|
<article class="feature-card">
|
||||||
|
<p class="feature-index"><?php echo esc_html( sprintf( '%02d', $index + 1 ) ); ?></p>
|
||||||
|
<h3><?php echo esc_html( $feature[0] ); ?></h3>
|
||||||
|
<p><?php echo esc_html( $feature[1] ); ?></p>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section section-sand" id="laboratorio" aria-labelledby="laboratory-title">
|
||||||
|
<div class="container split-grid">
|
||||||
|
<div class="editorial-image"><img src="<?php echo esc_url( $laboratory_image ); ?>" alt="Esercizio di movimento nello spazio" width="960" height="760" loading="lazy"></div>
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Pratica e ricerca</p>
|
||||||
|
<h2 id="laboratory-title"><?php echo esc_html( azionelab_mod( 'laboratory_title' ) ); ?></h2>
|
||||||
|
<div class="lead rich-text"><?php echo wp_kses_post( wpautop( azionelab_mod( 'laboratory_body' ) ) ); ?></div>
|
||||||
|
<ul class="practice-list" aria-label="Aree di lavoro"><li>Corpo</li><li>Voce</li><li>Improvvisazione</li><li>Ascolto</li><li>Presenza scenica</li></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="maestro" aria-labelledby="teacher-title">
|
||||||
|
<div class="container teacher-grid">
|
||||||
|
<div class="teacher-photo"><img src="<?php echo esc_url( $teacher_image ); ?>" alt="Ritratto di <?php echo esc_attr( azionelab_mod( 'teacher_name' ) ); ?>" width="760" height="900" loading="lazy"></div>
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">La guida del percorso</p>
|
||||||
|
<h2 id="teacher-title">Il maestro</h2>
|
||||||
|
<h3><?php echo esc_html( azionelab_mod( 'teacher_name' ) ); ?></h3>
|
||||||
|
<div class="lead rich-text"><?php echo wp_kses_post( wpautop( azionelab_mod( 'teacher_bio' ) ) ); ?></div>
|
||||||
|
<blockquote>“<?php echo esc_html( azionelab_mod( 'teacher_quote' ) ); ?>”</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="lessons section section-sand" id="lezioni" aria-labelledby="lessons-title">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-heading"><div><p class="eyebrow">Informazioni pratiche</p><h2 id="lessons-title">Le lezioni</h2></div><p>Un appuntamento settimanale per allenarsi con continuità, senza fretta.</p></div>
|
||||||
|
<div class="lesson-panel">
|
||||||
|
<dl class="lesson-details">
|
||||||
|
<div><dt>Quando</dt><dd><?php echo esc_html( azionelab_mod( 'lesson_schedule' ) ); ?></dd></div>
|
||||||
|
<div><dt>Dove</dt><dd><?php echo esc_html( azionelab_mod( 'lesson_location' ) ); ?></dd></div>
|
||||||
|
<div><dt>Per chi</dt><dd><?php echo esc_html( azionelab_mod( 'lesson_audience' ) ); ?></dd></div>
|
||||||
|
<div><dt>Cosa facciamo</dt><dd><?php echo wp_kses_post( azionelab_mod( 'lesson_description' ) ); ?></dd></div>
|
||||||
|
</dl>
|
||||||
|
<aside class="trial-card">
|
||||||
|
<p class="eyebrow">Puoi provare</p>
|
||||||
|
<h3><?php echo esc_html( azionelab_mod( 'trial_lesson' ) ); ?></h3>
|
||||||
|
<p><?php echo esc_html( azionelab_mod( 'lesson_notes' ) ); ?></p>
|
||||||
|
<a class="button" href="#contatti">Prenota un incontro</a>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="spettacoli" aria-labelledby="shows-title">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-heading"><div><p class="eyebrow">Spettacoli e progetti</p><h2 id="shows-title">La scena continua</h2></div><p>Esiti, attraversamenti e lavori corali nati durante il percorso.</p></div>
|
||||||
|
<div class="shows-grid">
|
||||||
|
<?php
|
||||||
|
$shows = new WP_Query(
|
||||||
|
array(
|
||||||
|
'post_type' => 'azl_show',
|
||||||
|
'posts_per_page' => 6,
|
||||||
|
'orderby' => array( 'menu_order' => 'ASC', 'date' => 'DESC' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if ( $shows->have_posts() ) :
|
||||||
|
$show_index = 0;
|
||||||
|
while ( $shows->have_posts() ) :
|
||||||
|
$shows->the_post();
|
||||||
|
$fallback = azionelab_asset( 0 === $show_index % 2 ? 'show-one.svg' : 'show-two.svg' );
|
||||||
|
$image = get_the_post_thumbnail_url( get_the_ID(), 'large' ) ?: $fallback;
|
||||||
|
$year = get_post_meta( get_the_ID(), 'azl_show_year', true );
|
||||||
|
$location = get_post_meta( get_the_ID(), 'azl_show_location', true );
|
||||||
|
?>
|
||||||
|
<article class="show-card">
|
||||||
|
<img src="<?php echo esc_url( $image ); ?>" alt="Locandina di <?php the_title_attribute(); ?>" width="700" height="880" loading="lazy">
|
||||||
|
<div class="show-copy"><p class="show-meta"><?php echo esc_html( trim( $year . ' · ' . $location, ' ·' ) ); ?></p><h3><?php the_title(); ?></h3><div><?php the_excerpt(); ?></div></div>
|
||||||
|
</article>
|
||||||
|
<?php
|
||||||
|
++$show_index;
|
||||||
|
endwhile;
|
||||||
|
wp_reset_postdata();
|
||||||
|
else :
|
||||||
|
?>
|
||||||
|
<p class="empty-state">I prossimi progetti saranno pubblicati presto.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section section-sand" id="galleria" aria-labelledby="gallery-title">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-heading"><div><p class="eyebrow">Dentro il laboratorio</p><h2 id="gallery-title">Galleria</h2></div><p>Lezioni, backstage, spettacoli e il tempo condiviso dal gruppo.</p></div>
|
||||||
|
<div class="gallery-grid">
|
||||||
|
<?php
|
||||||
|
$gallery = new WP_Query(
|
||||||
|
array(
|
||||||
|
'post_type' => 'azl_gallery',
|
||||||
|
'posts_per_page' => 8,
|
||||||
|
'orderby' => array( 'menu_order' => 'ASC', 'date' => 'DESC' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if ( $gallery->have_posts() ) :
|
||||||
|
$gallery_index = 0;
|
||||||
|
while ( $gallery->have_posts() ) :
|
||||||
|
$gallery->the_post();
|
||||||
|
$fallback = azionelab_asset( 'gallery-' . ( ( $gallery_index % 4 ) + 1 ) . '.svg' );
|
||||||
|
$image = get_the_post_thumbnail_url( get_the_ID(), 'large' ) ?: $fallback;
|
||||||
|
$category = get_post_meta( get_the_ID(), 'azl_gallery_category', true );
|
||||||
|
?>
|
||||||
|
<figure class="gallery-item"><img src="<?php echo esc_url( $image ); ?>" alt="<?php the_title_attribute(); ?>" width="860" height="700" loading="lazy"><figcaption><?php echo esc_html( $category ? $category . ' · ' . get_the_title() : get_the_title() ); ?></figcaption></figure>
|
||||||
|
<?php
|
||||||
|
++$gallery_index;
|
||||||
|
endwhile;
|
||||||
|
wp_reset_postdata();
|
||||||
|
else :
|
||||||
|
?>
|
||||||
|
<p class="empty-state">La galleria sarà aggiornata presto.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="contacts section" id="contatti" aria-labelledby="contacts-title">
|
||||||
|
<div class="container contact-grid">
|
||||||
|
<div><p class="eyebrow">Parliamone</p><h2 id="contacts-title">Vieni a conoscerci</h2><p class="lead">Non serve esperienza: basta la curiosità di incontrare il teatro e il gruppo.</p><p><strong>Dove</strong><br><?php echo esc_html( azionelab_mod( 'contact_address' ) ); ?></p><div class="social-links"><a href="<?php echo esc_url( azionelab_mod( 'contact_instagram' ) ); ?>" rel="noopener noreferrer">Instagram</a><a href="<?php echo esc_url( azionelab_mod( 'contact_facebook' ) ); ?>" rel="noopener noreferrer">Facebook</a></div></div>
|
||||||
|
<div class="contact-actions">
|
||||||
|
<a class="contact-action" href="mailto:<?php echo esc_attr( sanitize_email( azionelab_mod( 'contact_email' ) ) ); ?>"><span>Scrivi una mail</span><strong><?php echo esc_html( azionelab_mod( 'contact_email' ) ); ?></strong></a>
|
||||||
|
<a class="contact-action" href="tel:<?php echo esc_attr( azionelab_phone_href( azionelab_mod( 'contact_phone' ) ) ); ?>"><span>Chiama</span><strong><?php echo esc_html( azionelab_mod( 'contact_phone' ) ); ?></strong></a>
|
||||||
|
<a class="contact-action" href="https://wa.me/<?php echo esc_attr( azionelab_whatsapp_href( azionelab_mod( 'contact_whatsapp' ) ) ); ?>" rel="noopener noreferrer"><span>WhatsApp</span><strong><?php echo esc_html( azionelab_mod( 'contact_whatsapp' ) ); ?></strong></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?php
|
||||||
|
get_footer();
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Azione!Lab theme setup and editable homepage fields.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_defaults(): array {
|
||||||
|
return array(
|
||||||
|
'hero_title' => 'Il teatro diventa presenza.',
|
||||||
|
'hero_subtitle' => 'Un laboratorio dove corpo, voce, ascolto e relazione diventano scena.',
|
||||||
|
'hero_primary_label' => 'Vieni a conoscerci',
|
||||||
|
'hero_secondary_label'=> 'Guarda la galleria',
|
||||||
|
'intro_title' => 'Uno spazio per esserci davvero',
|
||||||
|
'intro_body' => 'Ogni incontro è uno spazio di pratica: si prova, si sbaglia, si ascolta, si ricomincia. Il gruppo diventa materia viva, la scena diventa occasione di scoperta.',
|
||||||
|
'laboratory_title' => 'Il laboratorio',
|
||||||
|
'laboratory_body' => 'Lavoriamo con il corpo, la voce e l’improvvisazione per allenare ascolto e presenza scenica. Attraverso esercizi individuali e di gruppo, ogni persona trova un modo autentico di stare nello spazio e nella relazione.',
|
||||||
|
'teacher_name' => 'Ernesto Estatico',
|
||||||
|
'teacher_bio' => 'Attore, regista e formatore, accompagna gruppi di ogni esperienza con cura e concretezza. Da oltre quindici anni conduce percorsi dedicati alla ricerca dell’autenticità scenica, intrecciando pedagogia teatrale, movimento e lavoro sulla voce.',
|
||||||
|
'teacher_quote' => 'Il teatro non è fingere: è imparare a essere presenti.',
|
||||||
|
'lesson_schedule' => 'Ogni martedì, dalle 20:00 alle 22:30',
|
||||||
|
'lesson_location' => "Spazio Teatro, Via dell'Epomeo 9999, Napoli",
|
||||||
|
'lesson_audience' => 'Adulti, con o senza esperienza',
|
||||||
|
'lesson_description' => 'Training fisico e vocale, improvvisazione, ascolto, costruzione del personaggio e lavoro di scena.',
|
||||||
|
'trial_lesson' => 'La prima lezione di prova è gratuita, su prenotazione.',
|
||||||
|
'lesson_notes' => 'Abiti comodi e calze antiscivolo consigliati.',
|
||||||
|
'contact_email' => 'ciao@azionelab.org',
|
||||||
|
'contact_phone' => '+39 333 123 4567',
|
||||||
|
'contact_whatsapp' => '+39 333 123 4567',
|
||||||
|
'contact_instagram' => 'https://instagram.com/',
|
||||||
|
'contact_facebook' => 'https://facebook.com/',
|
||||||
|
'contact_address' => "Via dell'Epomeo 9999, Napoli",
|
||||||
|
'footer_text' => 'Uno spazio aperto a chi desidera incontrare il teatro, insieme.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_mod( string $key ): string {
|
||||||
|
$defaults = azionelab_defaults();
|
||||||
|
return (string) get_theme_mod( $key, $defaults[ $key ] ?? '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_asset( string $filename ): string {
|
||||||
|
return get_theme_file_uri( 'assets/images/' . $filename );
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_phone_href( string $number ): string {
|
||||||
|
return preg_replace( '/[^0-9+]/', '', $number ) ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_whatsapp_href( string $number ): string {
|
||||||
|
return preg_replace( '/[^0-9]/', '', $number ) ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function azionelab_setup(): void {
|
||||||
|
load_theme_textdomain( 'azionelab', get_template_directory() . '/languages' );
|
||||||
|
add_theme_support( 'title-tag' );
|
||||||
|
add_theme_support( 'post-thumbnails' );
|
||||||
|
add_theme_support( 'responsive-embeds' );
|
||||||
|
add_theme_support(
|
||||||
|
'html5',
|
||||||
|
array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action( 'after_setup_theme', 'azionelab_setup' );
|
||||||
|
|
||||||
|
function azionelab_assets(): void {
|
||||||
|
wp_enqueue_style( 'azionelab', get_stylesheet_uri(), array(), '1.0.0' );
|
||||||
|
}
|
||||||
|
add_action( 'wp_enqueue_scripts', 'azionelab_assets' );
|
||||||
|
|
||||||
|
function azionelab_customize_register( WP_Customize_Manager $customizer ): void {
|
||||||
|
$defaults = azionelab_defaults();
|
||||||
|
$sections = array(
|
||||||
|
'azionelab_hero' => array( 'Hero', 30 ),
|
||||||
|
'azionelab_intro' => array( 'Manifesto e laboratorio', 31 ),
|
||||||
|
'azionelab_teacher' => array( 'Il maestro', 32 ),
|
||||||
|
'azionelab_lessons' => array( 'Le lezioni', 33 ),
|
||||||
|
'azionelab_contacts' => array( 'Contatti e footer', 34 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $sections as $id => $section ) {
|
||||||
|
$customizer->add_section(
|
||||||
|
$id,
|
||||||
|
array( 'title' => $section[0], 'priority' => $section[1] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = array(
|
||||||
|
'hero_title' => array( 'azionelab_hero', 'Titolo', 'text' ),
|
||||||
|
'hero_subtitle' => array( 'azionelab_hero', 'Sottotitolo', 'textarea' ),
|
||||||
|
'hero_image' => array( 'azionelab_hero', 'Immagine principale', 'image' ),
|
||||||
|
'hero_primary_label' => array( 'azionelab_hero', 'Etichetta CTA primaria', 'text' ),
|
||||||
|
'hero_secondary_label' => array( 'azionelab_hero', 'Etichetta CTA secondaria', 'text' ),
|
||||||
|
'intro_title' => array( 'azionelab_intro', 'Titolo manifesto', 'text' ),
|
||||||
|
'intro_body' => array( 'azionelab_intro', 'Testo manifesto', 'textarea_html' ),
|
||||||
|
'laboratory_title' => array( 'azionelab_intro', 'Titolo laboratorio', 'text' ),
|
||||||
|
'laboratory_body' => array( 'azionelab_intro', 'Descrizione laboratorio', 'textarea_html' ),
|
||||||
|
'laboratory_image' => array( 'azionelab_intro', 'Immagine laboratorio', 'image' ),
|
||||||
|
'teacher_name' => array( 'azionelab_teacher', 'Nome', 'text' ),
|
||||||
|
'teacher_bio' => array( 'azionelab_teacher', 'Biografia', 'textarea_html' ),
|
||||||
|
'teacher_quote' => array( 'azionelab_teacher', 'Citazione', 'textarea' ),
|
||||||
|
'teacher_image' => array( 'azionelab_teacher', 'Foto', 'image' ),
|
||||||
|
'lesson_schedule' => array( 'azionelab_lessons', 'Quando', 'text' ),
|
||||||
|
'lesson_location' => array( 'azionelab_lessons', 'Dove', 'text' ),
|
||||||
|
'lesson_audience' => array( 'azionelab_lessons', 'Per chi', 'text' ),
|
||||||
|
'lesson_description' => array( 'azionelab_lessons', 'Cosa si fa', 'textarea_html' ),
|
||||||
|
'trial_lesson' => array( 'azionelab_lessons', 'Lezione di prova', 'text' ),
|
||||||
|
'lesson_notes' => array( 'azionelab_lessons', 'Note', 'textarea' ),
|
||||||
|
'contact_email' => array( 'azionelab_contacts', 'Email', 'email' ),
|
||||||
|
'contact_phone' => array( 'azionelab_contacts', 'Telefono', 'text' ),
|
||||||
|
'contact_whatsapp' => array( 'azionelab_contacts', 'WhatsApp', 'text' ),
|
||||||
|
'contact_instagram' => array( 'azionelab_contacts', 'Instagram', 'url' ),
|
||||||
|
'contact_facebook' => array( 'azionelab_contacts', 'Facebook', 'url' ),
|
||||||
|
'contact_address' => array( 'azionelab_contacts', 'Indirizzo', 'text' ),
|
||||||
|
'footer_text' => array( 'azionelab_contacts', 'Testo footer', 'text' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $fields as $id => $field ) {
|
||||||
|
$type = $field[2];
|
||||||
|
$sanitize_callback = 'sanitize_text_field';
|
||||||
|
if ( 'textarea' === $type ) {
|
||||||
|
$sanitize_callback = 'sanitize_textarea_field';
|
||||||
|
} elseif ( 'textarea_html' === $type ) {
|
||||||
|
$sanitize_callback = 'wp_kses_post';
|
||||||
|
} elseif ( 'email' === $type ) {
|
||||||
|
$sanitize_callback = 'sanitize_email';
|
||||||
|
} elseif ( 'url' === $type || 'image' === $type ) {
|
||||||
|
$sanitize_callback = 'esc_url_raw';
|
||||||
|
}
|
||||||
|
|
||||||
|
$customizer->add_setting(
|
||||||
|
$id,
|
||||||
|
array(
|
||||||
|
'default' => $defaults[ $id ] ?? '',
|
||||||
|
'sanitize_callback' => $sanitize_callback,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( 'image' === $type ) {
|
||||||
|
$customizer->add_control(
|
||||||
|
new WP_Customize_Image_Control(
|
||||||
|
$customizer,
|
||||||
|
$id,
|
||||||
|
array( 'label' => $field[1], 'section' => $field[0] )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$customizer->add_control(
|
||||||
|
$id,
|
||||||
|
array(
|
||||||
|
'label' => $field[1],
|
||||||
|
'section' => $field[0],
|
||||||
|
'type' => str_starts_with( $type, 'textarea' ) ? 'textarea' : $type,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action( 'customize_register', 'azionelab_customize_register' );
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
/** Site header. */
|
||||||
|
|
||||||
|
$site_name = get_bloginfo( 'name' ) ?: 'Azione!Lab';
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html <?php language_attributes(); ?>>
|
||||||
|
<head>
|
||||||
|
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#F3EBDD">
|
||||||
|
<?php wp_head(); ?>
|
||||||
|
</head>
|
||||||
|
<body <?php body_class(); ?>>
|
||||||
|
<?php wp_body_open(); ?>
|
||||||
|
<a class="skip-link" href="#contenuto"><?php esc_html_e( 'Vai al contenuto', 'azionelab' ); ?></a>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container header-inner">
|
||||||
|
<a class="wordmark" href="#inizio" aria-label="<?php echo esc_attr( $site_name . ', torna all’inizio' ); ?>">
|
||||||
|
<?php echo esc_html( $site_name ); ?>
|
||||||
|
</a>
|
||||||
|
<nav class="desktop-nav" aria-label="<?php esc_attr_e( 'Navigazione principale', 'azionelab' ); ?>">
|
||||||
|
<a href="#laboratorio">Il laboratorio</a>
|
||||||
|
<a href="#maestro">Il maestro</a>
|
||||||
|
<a href="#lezioni">Le lezioni</a>
|
||||||
|
<a href="#spettacoli">Progetti</a>
|
||||||
|
<a href="#galleria">Galleria</a>
|
||||||
|
</nav>
|
||||||
|
<a class="button button-small desktop-cta" href="#contatti">Vieni a conoscerci</a>
|
||||||
|
<details class="mobile-menu">
|
||||||
|
<summary><span class="menu-icon" aria-hidden="true"></span><span>Menu</span></summary>
|
||||||
|
<nav aria-label="<?php esc_attr_e( 'Navigazione mobile', 'azionelab' ); ?>">
|
||||||
|
<a href="#laboratorio">Il laboratorio</a>
|
||||||
|
<a href="#maestro">Il maestro</a>
|
||||||
|
<a href="#lezioni">Le lezioni</a>
|
||||||
|
<a href="#spettacoli">Progetti</a>
|
||||||
|
<a href="#galleria">Galleria</a>
|
||||||
|
<a class="button button-small" href="#contatti">Vieni a conoscerci</a>
|
||||||
|
</nav>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/** Fallback template. */
|
||||||
|
|
||||||
|
get_header();
|
||||||
|
?>
|
||||||
|
<main id="contenuto" class="section">
|
||||||
|
<div class="container narrow">
|
||||||
|
<?php if ( have_posts() ) : ?>
|
||||||
|
<?php while ( have_posts() ) : the_post(); ?>
|
||||||
|
<article <?php post_class(); ?>>
|
||||||
|
<h1><?php the_title(); ?></h1>
|
||||||
|
<div class="rich-text"><?php the_content(); ?></div>
|
||||||
|
</article>
|
||||||
|
<?php endwhile; ?>
|
||||||
|
<?php else : ?>
|
||||||
|
<p><?php esc_html_e( 'Nessun contenuto disponibile.', 'azionelab' ); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<?php
|
||||||
|
get_footer();
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
Theme Name: Azione!Lab
|
||||||
|
Theme URI: https://azionelab.org
|
||||||
|
Author: Azione!Lab
|
||||||
|
Description: Editorial single-page theme for the Azione!Lab theatre workshop.
|
||||||
|
Version: 1.0.0
|
||||||
|
Requires at least: 7.0
|
||||||
|
Requires PHP: 8.3
|
||||||
|
Text Domain: azionelab
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-bg: #f3ebdd;
|
||||||
|
--color-bg-light: #fff8ef;
|
||||||
|
--color-dark: #4a433a;
|
||||||
|
--color-dark-soft: #5a5046;
|
||||||
|
--color-dark-text: #1d1a17;
|
||||||
|
--color-primary: #a4513b;
|
||||||
|
--color-primary-soft: #b8674c;
|
||||||
|
--color-sage: #515d53;
|
||||||
|
--color-sand: #d8c7ae;
|
||||||
|
--color-muted: #7a7067;
|
||||||
|
--color-border: #e6d8c8;
|
||||||
|
--font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
||||||
|
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
--shadow: 0 24px 70px rgba(74, 67, 58, 0.14);
|
||||||
|
--radius: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-dark-text);
|
||||||
|
background: var(--color-bg);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
content: "";
|
||||||
|
opacity: 0.28;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: radial-gradient(rgba(74, 67, 58, 0.1) 0.6px, transparent 0.6px);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
}
|
||||||
|
img { display: block; width: 100%; height: auto; }
|
||||||
|
a { color: inherit; }
|
||||||
|
h1, h2, h3, p { margin-top: 0; }
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 0.98;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
h1 { max-width: 10ch; margin-bottom: 1.5rem; font-size: clamp(3.4rem, 12vw, 7.8rem); }
|
||||||
|
h2 { margin-bottom: 1.5rem; font-size: clamp(2.7rem, 8vw, 5.25rem); }
|
||||||
|
h3 { margin-bottom: 0.75rem; font-size: clamp(1.65rem, 4vw, 2.35rem); }
|
||||||
|
.container { width: min(100% - 2rem, 76rem); margin-inline: auto; }
|
||||||
|
.narrow { width: min(100%, 48rem); }
|
||||||
|
.section { padding-block: clamp(4.5rem, 10vw, 8.5rem); }
|
||||||
|
.section-sand { background: rgba(216, 199, 174, 0.5); }
|
||||||
|
.eyebrow {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.lead { max-width: 42rem; color: var(--color-dark-soft); font-size: clamp(1.14rem, 2.5vw, 1.42rem); }
|
||||||
|
.rich-text > :last-child { margin-bottom: 0; }
|
||||||
|
.skip-link {
|
||||||
|
position: fixed;
|
||||||
|
top: 0.75rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
color: white;
|
||||||
|
background: var(--color-dark-text);
|
||||||
|
transform: translateY(-200%);
|
||||||
|
}
|
||||||
|
.skip-link:focus { transform: none; }
|
||||||
|
.site-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
border-bottom: 1px solid rgba(230, 216, 200, 0.8);
|
||||||
|
background: rgba(243, 235, 221, 0.93);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
.header-inner { display: flex; min-height: 4.7rem; align-items: center; justify-content: space-between; gap: 1rem; }
|
||||||
|
.wordmark { font-family: var(--font-serif); font-size: 1.65rem; font-weight: 700; text-decoration: none; }
|
||||||
|
.desktop-nav { display: none; gap: 1.6rem; font-size: 0.92rem; }
|
||||||
|
.desktop-nav a { text-decoration: none; }
|
||||||
|
.desktop-nav a:hover { color: var(--color-primary); }
|
||||||
|
.desktop-cta { display: none !important; }
|
||||||
|
.mobile-menu { position: relative; }
|
||||||
|
.mobile-menu summary { display: flex; cursor: pointer; align-items: center; gap: 0.6rem; font-weight: 700; list-style: none; }
|
||||||
|
.mobile-menu summary::-webkit-details-marker { display: none; }
|
||||||
|
.menu-icon, .menu-icon::before, .menu-icon::after { display: block; width: 1.35rem; height: 2px; background: currentColor; }
|
||||||
|
.menu-icon { position: relative; }
|
||||||
|
.menu-icon::before, .menu-icon::after { position: absolute; left: 0; content: ""; }
|
||||||
|
.menu-icon::before { top: -0.38rem; }
|
||||||
|
.menu-icon::after { top: 0.38rem; }
|
||||||
|
.mobile-menu nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 2.35rem;
|
||||||
|
right: 0;
|
||||||
|
display: grid;
|
||||||
|
width: min(20rem, calc(100vw - 2rem));
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--color-bg-light);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
.mobile-menu nav a { padding: 0.65rem; text-decoration: none; }
|
||||||
|
.button-row { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.4rem; }
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: 999px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
background: var(--color-primary);
|
||||||
|
font-weight: 750;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 180ms ease, background 180ms ease;
|
||||||
|
}
|
||||||
|
.button:hover { background: var(--color-primary-soft); transform: translateY(-2px); }
|
||||||
|
.button:focus-visible, a:focus-visible, summary:focus-visible { outline: 3px solid var(--color-sage); outline-offset: 4px; }
|
||||||
|
.button-small { min-height: 2.5rem; padding: 0.55rem 1rem; font-size: 0.9rem; }
|
||||||
|
.text-link { color: var(--color-dark); font-weight: 750; text-underline-offset: 0.3em; }
|
||||||
|
.hero { padding-top: clamp(3rem, 8vw, 6.5rem); }
|
||||||
|
.hero-grid { display: grid; align-items: center; gap: 3rem; }
|
||||||
|
.hero-subtitle { max-width: 35rem; margin-bottom: 2rem; color: var(--color-dark-soft); font-size: clamp(1.15rem, 3vw, 1.45rem); }
|
||||||
|
.hero-visual { position: relative; margin: 0; }
|
||||||
|
.hero-visual img { aspect-ratio: 5 / 6; border-radius: 48% 48% 1.5rem 1.5rem; object-fit: cover; box-shadow: var(--shadow); }
|
||||||
|
.hero-visual figcaption { position: absolute; right: -0.25rem; bottom: 1.25rem; padding: 0.65rem 0.9rem; background: var(--color-bg-light); font-family: var(--font-serif); font-style: italic; }
|
||||||
|
.manifesto { position: relative; text-align: center; }
|
||||||
|
.manifesto::before { display: block; width: 4rem; height: 1px; margin: 0 auto 2rem; content: ""; background: var(--color-primary); }
|
||||||
|
.manifesto-text { font-family: var(--font-serif); font-size: clamp(1.55rem, 4vw, 2.6rem); line-height: 1.25; }
|
||||||
|
.features { padding-top: 0; }
|
||||||
|
.feature-grid { display: grid; gap: 1rem; }
|
||||||
|
.feature-card { min-height: 15rem; padding: 2rem; border: 1px solid var(--color-border); border-radius: var(--radius); background: rgba(255, 248, 239, 0.72); }
|
||||||
|
.feature-index { color: var(--color-primary); font-family: var(--font-serif); font-size: 1.2rem; }
|
||||||
|
.split-grid, .teacher-grid { display: grid; align-items: center; gap: 3rem; }
|
||||||
|
.editorial-image img, .teacher-photo img { aspect-ratio: 5 / 4; border-radius: var(--radius); object-fit: cover; box-shadow: var(--shadow); }
|
||||||
|
.teacher-photo img { aspect-ratio: 4 / 5; }
|
||||||
|
.practice-list { display: flex; margin: 2rem 0 0; padding: 0; flex-wrap: wrap; gap: 0.6rem; list-style: none; }
|
||||||
|
.practice-list li { padding: 0.45rem 0.8rem; border: 1px solid var(--color-sand); border-radius: 999px; background: var(--color-bg-light); }
|
||||||
|
blockquote { margin: 2rem 0 0; padding-left: 1.5rem; border-left: 3px solid var(--color-primary); font-family: var(--font-serif); font-size: clamp(1.5rem, 3vw, 2.25rem); font-style: italic; }
|
||||||
|
.section-heading { margin-bottom: 2.5rem; }
|
||||||
|
.lesson-panel { display: grid; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-bg-light); box-shadow: var(--shadow); }
|
||||||
|
.lesson-details { display: grid; margin: 0; padding: 2rem; gap: 1.5rem; }
|
||||||
|
.lesson-details div { padding-bottom: 1.2rem; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.lesson-details div:last-child { border-bottom: 0; }
|
||||||
|
.lesson-details dt { margin-bottom: 0.25rem; color: var(--color-primary); font-weight: 750; }
|
||||||
|
.lesson-details dd { margin: 0; }
|
||||||
|
.trial-card { padding: 2rem; color: var(--color-bg-light); background: var(--color-sage); }
|
||||||
|
.trial-card .eyebrow { color: var(--color-sand); }
|
||||||
|
.shows-grid { display: grid; gap: 1.25rem; }
|
||||||
|
.show-card { overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-bg-light); }
|
||||||
|
.show-card img { aspect-ratio: 4 / 5; object-fit: cover; }
|
||||||
|
.show-copy { padding: 1.4rem; }
|
||||||
|
.show-meta { color: var(--color-primary); font-size: 0.85rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||||
|
.gallery-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
|
||||||
|
.gallery-item { position: relative; overflow: hidden; margin: 0; border-radius: 1rem; background: var(--color-sand); }
|
||||||
|
.gallery-item img { aspect-ratio: 1 / 1; object-fit: cover; transition: transform 300ms ease; }
|
||||||
|
.gallery-item:hover img { transform: scale(1.025); }
|
||||||
|
.gallery-item figcaption { position: absolute; right: 0.6rem; bottom: 0.6rem; left: 0.6rem; padding: 0.5rem 0.7rem; border-radius: 0.65rem; color: white; background: rgba(29, 26, 23, 0.76); font-size: 0.84rem; }
|
||||||
|
.empty-state { padding: 2rem; border: 1px dashed var(--color-muted); border-radius: var(--radius); color: var(--color-muted); text-align: center; }
|
||||||
|
.contacts { color: var(--color-bg-light); background: var(--color-dark); }
|
||||||
|
.contacts .eyebrow { color: var(--color-primary-soft); }
|
||||||
|
.contact-grid { display: grid; gap: 2.5rem; }
|
||||||
|
.contact-actions { display: grid; gap: 0.8rem; }
|
||||||
|
.contact-action { display: grid; padding: 1rem 1.1rem; border: 1px solid rgba(255,255,255,0.18); border-radius: 0.9rem; text-decoration: none; }
|
||||||
|
.contact-action span { color: var(--color-sand); font-size: 0.78rem; text-transform: uppercase; }
|
||||||
|
.social-links { display: flex; margin-top: 1.25rem; flex-wrap: wrap; gap: 1rem; }
|
||||||
|
.site-footer { padding-block: 2rem; color: var(--color-bg-light); background: var(--color-dark-text); }
|
||||||
|
.footer-inner { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem 1.5rem; font-size: 0.85rem; }
|
||||||
|
.footer-inner p { margin: 0; }
|
||||||
|
|
||||||
|
@media (min-width: 44rem) {
|
||||||
|
.container { width: min(100% - 3rem, 76rem); }
|
||||||
|
.hero-grid, .split-grid, .teacher-grid { grid-template-columns: 1fr 0.82fr; gap: clamp(3rem, 7vw, 6rem); }
|
||||||
|
.split-grid { grid-template-columns: 0.95fr 1.05fr; }
|
||||||
|
.feature-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
.lesson-panel { grid-template-columns: 1.5fr 0.8fr; }
|
||||||
|
.lesson-details { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.shows-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.gallery-grid { grid-template-columns: repeat(4, 1fr); }
|
||||||
|
.gallery-item:nth-child(3n + 1) { grid-column: span 2; }
|
||||||
|
.contact-grid { grid-template-columns: 1fr 0.9fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 64rem) {
|
||||||
|
.desktop-nav { display: flex; }
|
||||||
|
.desktop-cta { display: inline-flex !important; }
|
||||||
|
.mobile-menu { display: none; }
|
||||||
|
.shows-grid { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
.section-heading { display: flex; align-items: end; justify-content: space-between; gap: 2rem; }
|
||||||
|
.section-heading > p { max-width: 31rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd /var/www/html
|
||||||
|
|
||||||
|
if [ "${WP_ENVIRONMENT_TYPE:-local}" = production ]; then
|
||||||
|
admin_password="${WP_ADMIN_PASSWORD:-}"
|
||||||
|
case "$admin_password" in
|
||||||
|
"" | *replace-with*)
|
||||||
|
echo "A non-placeholder WP_ADMIN_PASSWORD is required in production." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ "${#admin_password}" -lt 16 ]; then
|
||||||
|
echo "WP_ADMIN_PASSWORD must contain at least 16 characters in production." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempt=1
|
||||||
|
until wp db check >/dev/null 2>&1; do
|
||||||
|
if [ "$attempt" -ge 30 ]; then
|
||||||
|
echo "WordPress database was not ready after $attempt attempts." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! wp core is-installed; then
|
||||||
|
wp core install \
|
||||||
|
--url="${WP_URL:?WP_URL is required}" \
|
||||||
|
--title="${WP_TITLE:-Azione!Lab}" \
|
||||||
|
--admin_user="${WP_ADMIN_USER:?WP_ADMIN_USER is required}" \
|
||||||
|
--admin_password="${WP_ADMIN_PASSWORD:?WP_ADMIN_PASSWORD is required}" \
|
||||||
|
--admin_email="${WP_ADMIN_EMAIL:?WP_ADMIN_EMAIL is required}" \
|
||||||
|
--skip-email
|
||||||
|
fi
|
||||||
|
|
||||||
|
wp option update home "$WP_URL"
|
||||||
|
wp option update siteurl "$WP_URL"
|
||||||
|
wp option update blogname "${WP_TITLE:-Azione!Lab}"
|
||||||
|
wp option update blogdescription "Il teatro diventa presenza."
|
||||||
|
wp option update timezone_string "Europe/Rome"
|
||||||
|
wp option update permalink_structure '/%postname%/'
|
||||||
|
if ! wp theme is-active azionelab; then
|
||||||
|
wp theme activate azionelab
|
||||||
|
fi
|
||||||
|
|
||||||
|
for sample_id in 1 2; do
|
||||||
|
if ! wp post get "$sample_id" --field=ID >/dev/null 2>&1; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
sample_slug="$(wp post get "$sample_id" --field=post_name)"
|
||||||
|
case "$sample_slug" in
|
||||||
|
hello-world | sample-page)
|
||||||
|
wp post delete "$sample_id" --force >/dev/null
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ensure_show() {
|
||||||
|
slug="$1"
|
||||||
|
title="$2"
|
||||||
|
excerpt="$3"
|
||||||
|
year="$4"
|
||||||
|
location="$5"
|
||||||
|
order="$6"
|
||||||
|
post_id="$(wp post list --post_type=azl_show --name="$slug" --field=ID --format=ids)"
|
||||||
|
if [ -z "$post_id" ]; then
|
||||||
|
post_id="$(wp post create --post_type=azl_show --post_status=publish --post_name="$slug" --post_title="$title" --post_excerpt="$excerpt" --menu_order="$order" --porcelain)"
|
||||||
|
fi
|
||||||
|
wp post meta update "$post_id" azl_show_year "$year" >/dev/null
|
||||||
|
wp post meta update "$post_id" azl_show_location "$location" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_gallery() {
|
||||||
|
slug="$1"
|
||||||
|
title="$2"
|
||||||
|
category="$3"
|
||||||
|
order="$4"
|
||||||
|
post_id="$(wp post list --post_type=azl_gallery --name="$slug" --field=ID --format=ids)"
|
||||||
|
if [ -z "$post_id" ]; then
|
||||||
|
post_id="$(wp post create --post_type=azl_gallery --post_status=publish --post_name="$slug" --post_title="$title" --menu_order="$order" --porcelain)"
|
||||||
|
fi
|
||||||
|
wp post meta update "$post_id" azl_gallery_category "$category" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_show "le-cose-che-restano" "Le cose che restano" "Un lavoro corale su memoria, gesti quotidiani e piccoli cambiamenti." "2025" "Teatro di Quartiere" "1"
|
||||||
|
ensure_show "fuori-campo" "Fuori campo" "Storie ai margini della scena che chiedono, finalmente, di essere ascoltate." "2024" "Spazio Scena" "2"
|
||||||
|
|
||||||
|
ensure_gallery "ascolto-e-movimento" "Ascolto e movimento" "Lezioni" "1"
|
||||||
|
ensure_gallery "prima-della-scena" "Prima di entrare in scena" "Backstage" "2"
|
||||||
|
ensure_gallery "spettacolo-finale" "Le cose che restano" "Spettacoli" "3"
|
||||||
|
ensure_gallery "il-gruppo" "Il laboratorio, insieme" "Gruppo" "4"
|
||||||
|
|
||||||
|
wp rewrite flush >/dev/null
|
||||||
|
echo "Azione!Lab WordPress content is ready."
|
||||||