feat: add wordpress site variant

This commit is contained in:
bisco
2026-06-25 07:36:25 +02:00
parent cae9180bc6
commit c66dd7e511
46 changed files with 2091 additions and 136 deletions
+11 -7
View File
@@ -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:
+24
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
.env
*.log
__pycache__/
*.py[cod]
backups/
tests/functional/test-results/
tests/functional/playwright-report/
+114 -61
View File
@@ -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.
+31
View File
@@ -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
+29
View File
@@ -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 "$@"
+47
View File
@@ -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:
+188
View File
@@ -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:
+58
View File
@@ -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.
+19 -9
View File
@@ -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).
+37 -11
View File
@@ -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.
+37 -9
View File
@@ -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.
+21 -10
View File
@@ -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.
+29 -13
View File
@@ -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.
+17 -16
View File
@@ -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.
+88
View File
@@ -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
) &
+10
View File
@@ -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
+54
View File
@@ -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;
+51
View File
@@ -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;
}
+26
View File
@@ -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;
}
+12
View File
@@ -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"]
+78
View File
@@ -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"
}
}
}
}
+11
View File
@@ -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"
}
}
+20
View File
@@ -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",
},
});
+115
View File
@@ -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();
});
+11
View File
@@ -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
+14
View File
@@ -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"]
+40
View File
@@ -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 "$@"
+128
View File
@@ -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' );
+9
View File
@@ -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

+22
View File
@@ -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>
+185
View File
@@ -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();
+161
View File
@@ -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 limprovvisazione 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 dellautenticità 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' );
+42
View File
@@ -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 allinizio' ); ?>">
<?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>
+21
View File
@@ -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();
+217
View File
@@ -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; }
}
+98
View File
@@ -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."