Restructure repository into administration and website areas

This commit is contained in:
2026-03-06 15:00:13 +01:00
parent 7c49898b29
commit ed192d79c2
75 changed files with 179 additions and 157 deletions

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: $0 <route-name> [domain]"
echo "Example: $0 webpage4 mydomain.de"
exit 1
fi
NAME="$1"
DOMAIN="${2:-mydomain.de}"
if [[ ! "$NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
echo "Invalid route name: $NAME"
echo "Allowed: letters, numbers, underscore, dash"
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ROOT_BASE="${ROOT_BASE:-/srv/ikfreunde}"
COMPOSE_FILE="${COMPOSE_FILE:-$ROOT_DIR/administration/docker-compose.traefik-routes.yml}"
ROOT="${ROOT_BASE}/${NAME}"
if [[ ! -f "$COMPOSE_FILE" ]]; then
echo "Compose file not found: $COMPOSE_FILE"
exit 1
fi
mkdir -p "$ROOT"
if [[ ! -f "$ROOT/ikfreunde.com.html" ]]; then
cp "$ROOT_DIR/website/ikfreunde.com.html" "$ROOT/ikfreunde.com.html"
echo "Created: $ROOT/ikfreunde.com.html"
fi
if [[ ! -f "$ROOT/site-content.de.json" ]]; then
cp "$ROOT_DIR/website/content/site-content.de.json" "$ROOT/site-content.de.json"
echo "Created: $ROOT/site-content.de.json"
fi
if rg -q "^ ${NAME}:$" "$COMPOSE_FILE"; then
echo "Service '${NAME}' already exists in $COMPOSE_FILE"
else
block_file="$(mktemp)"
tmp_file="$(mktemp)"
cat > "$block_file" <<YAML
${NAME}:
build:
context: ..
dockerfile: administration/Dockerfile
container_name: ikfreunde-${NAME}
volumes:
- ${ROOT}/ikfreunde.com.html:/app/website/ikfreunde.com.html
- ${ROOT}/site-content.de.json:/app/website/content/site-content.de.json
restart: unless-stopped
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.${NAME}.rule=Host(\`${DOMAIN}\`) && PathPrefix(\`/${NAME}\`)
- traefik.http.routers.${NAME}.entrypoints=websecure
- traefik.http.routers.${NAME}.tls=true
- traefik.http.services.${NAME}.loadbalancer.server.port=4173
- traefik.http.routers.${NAME}.middlewares=${NAME}-slash,${NAME}-strip
- traefik.http.middlewares.${NAME}-slash.redirectregex.regex=^https?://([^/]+)/${NAME}$
- traefik.http.middlewares.${NAME}-slash.redirectregex.replacement=https://\$\${1}/${NAME}/
- traefik.http.middlewares.${NAME}-slash.redirectregex.permanent=true
- traefik.http.middlewares.${NAME}-strip.stripprefix.prefixes=/${NAME}
YAML
awk -v block_file="$block_file" '
BEGIN { inserted = 0 }
/^networks:/ && inserted == 0 {
while ((getline line < block_file) > 0) print line
close(block_file)
inserted = 1
}
{ print }
END {
if (inserted == 0) {
while ((getline line < block_file) > 0) print line
close(block_file)
}
}
' "$COMPOSE_FILE" > "$tmp_file"
mv "$tmp_file" "$COMPOSE_FILE"
rm -f "$block_file"
echo "Inserted service '${NAME}' into $COMPOSE_FILE"
fi
echo
echo "Next steps:"
echo "1) docker compose -f $COMPOSE_FILE up -d --build"
echo "2) Open: https://${DOMAIN}/${NAME}/"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
INPUT_HTML="${1:-website/ikfreunde.com.html}"
OUTPUT_JSON="${2:-website/content/site-content.de.json}"
mkdir -p "$(dirname "$OUTPUT_JSON")"
php -d opcache.enable_cli=0 administration/scripts/extract_dom_content.php "$INPUT_HTML" "$OUTPUT_JSON"

View File

@@ -0,0 +1,318 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
if ($argc < 3) {
fwrite(STDERR, "Usage: php administration/scripts/extract_dom_content.php <input_html> <output_json>\n");
exit(1);
}
$inputHtml = $argv[1];
$outputJson = $argv[2];
if (!is_file($inputHtml)) {
fwrite(STDERR, "Input file not found: {$inputHtml}\n");
exit(1);
}
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$html = file_get_contents($inputHtml);
if ($html === false) {
fwrite(STDERR, "Failed to read input file: {$inputHtml}\n");
exit(1);
}
$loaded = $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET);
if (!$loaded) {
fwrite(STDERR, "Failed to parse HTML: {$inputHtml}\n");
exit(1);
}
$xpath = new DOMXPath($dom);
$data = [
'meta' => extractMeta($xpath, $dom),
'shared' => extractShared($xpath),
'sections' => extractMainSections($xpath),
];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($json === false) {
fwrite(STDERR, "Failed to encode JSON output.\n");
exit(1);
}
if (file_put_contents($outputJson, $json . "\n") === false) {
fwrite(STDERR, "Failed to write output file: {$outputJson}\n");
exit(1);
}
fwrite(STDOUT, "Wrote extracted content to {$outputJson}\n");
function extractMeta(DOMXPath $xpath, DOMDocument $dom): array
{
$meta = [
'title' => normalizeText($dom->getElementsByTagName('title')->item(0)?->textContent ?? ''),
'description' => firstAttrValue($xpath, "//meta[@name='description']", 'content'),
'open_graph' => [],
'twitter' => [],
'other' => [],
];
$ogNodes = $xpath->query("//meta[starts-with(@property, 'og:') or starts-with(@name, 'og:')]");
if ($ogNodes instanceof DOMNodeList) {
foreach ($ogNodes as $node) {
if (!$node instanceof DOMElement) {
continue;
}
$name = trim((string) ($node->getAttribute('property') ?: $node->getAttribute('name')));
$value = normalizeText((string) $node->getAttribute('content'));
if ($name !== '' && $value !== '') {
$meta['open_graph'][$name] = $value;
}
}
}
$twitterNodes = $xpath->query("//meta[starts-with(@name, 'twitter:')]");
if ($twitterNodes instanceof DOMNodeList) {
foreach ($twitterNodes as $node) {
if (!$node instanceof DOMElement) {
continue;
}
$name = trim((string) $node->getAttribute('name'));
$value = normalizeText((string) $node->getAttribute('content'));
if ($name !== '' && $value !== '') {
$meta['twitter'][$name] = $value;
}
}
}
$otherMetaNames = ['title'];
foreach ($otherMetaNames as $metaName) {
$value = firstAttrValue($xpath, "//meta[@name='{$metaName}']", 'content');
if ($value !== '') {
$meta['other'][$metaName] = $value;
}
}
ksort($meta['open_graph']);
ksort($meta['twitter']);
ksort($meta['other']);
return $meta;
}
function extractShared(DOMXPath $xpath): array
{
$commonTexts = [];
$textToKey = [];
$shared = [
'common_texts' => [],
'navigation' => ['text_keys' => [], 'images' => []],
'cookie_layer' => ['text_keys' => [], 'images' => []],
'footer' => ['text_keys' => [], 'images' => []],
];
$sharedRoots = [
'navigation' => firstNode($xpath, "//header[contains(@class, 'page-head')]"),
'cookie_layer' => firstNode($xpath, "//*[@id='cookie-layer']"),
'footer' => firstNode($xpath, "//footer[contains(@class, 'site-footer')]"),
];
foreach ($sharedRoots as $name => $root) {
if (!$root instanceof DOMNode) {
continue;
}
$texts = extractTexts($xpath, $root);
foreach ($texts as $text) {
if (!isset($textToKey[$text])) {
$key = 'common_' . str_pad((string) (count($commonTexts) + 1), 3, '0', STR_PAD_LEFT);
$textToKey[$text] = $key;
$commonTexts[$key] = $text;
}
$shared[$name]['text_keys'][] = $textToKey[$text];
}
$shared[$name]['images'] = extractImages($xpath, $root);
}
$shared['common_texts'] = $commonTexts;
return $shared;
}
function extractMainSections(DOMXPath $xpath): array
{
$sections = [];
$sectionQueries = [
'hero' => "//main//section[contains(@class, 'module-hero-teaser')]",
'page_header' => "//main//section[contains(@class, 'page-header')]",
'projects' => "//main//section[contains(@class, 'module-projects-teaser')]",
'services' => "//main//section[.//*[contains(@class, 'services-teaser__content')]]",
'team' => "(//main//section[contains(@class, 'text-image')])[1]",
'awards' => "//main//section[.//*[contains(@class, 'awards-teaser__content')]]",
'contact' => "//main//section[contains(@class, 'contact-teaser')]",
'clients' => "//main//section[.//*[contains(@class, 'clients-teaser__content')]]",
'partners' => "(//main//section[contains(@class, 'text-image')])[2]",
];
foreach ($sectionQueries as $name => $query) {
$root = firstNode($xpath, $query);
if (!$root instanceof DOMNode) {
continue;
}
$sections[$name] = [
'texts' => extractKeyValueTexts($xpath, $root),
'images' => extractImages($xpath, $root),
];
}
return $sections;
}
function extractKeyValueTexts(DOMXPath $xpath, DOMNode $root): array
{
$textNodes = $xpath->query('.//text()', $root);
if (!$textNodes instanceof DOMNodeList) {
return [];
}
$counters = [];
$texts = [];
foreach ($textNodes as $textNode) {
if (!$textNode instanceof DOMText) {
continue;
}
$value = normalizeText($textNode->wholeText);
if ($value === '') {
continue;
}
if (!isVisibleTextNode($textNode)) {
continue;
}
$parent = $textNode->parentNode;
if (!$parent instanceof DOMElement) {
continue;
}
$tag = strtolower($parent->tagName);
$counters[$tag] = ($counters[$tag] ?? 0) + 1;
$key = $tag . '_' . str_pad((string) $counters[$tag], 3, '0', STR_PAD_LEFT);
$texts[$key] = $value;
}
return $texts;
}
function extractTexts(DOMXPath $xpath, DOMNode $root): array
{
$textNodes = $xpath->query('.//text()', $root);
if (!$textNodes instanceof DOMNodeList) {
return [];
}
$texts = [];
foreach ($textNodes as $textNode) {
if (!$textNode instanceof DOMText) {
continue;
}
$value = normalizeText($textNode->wholeText);
if ($value === '') {
continue;
}
if (!isVisibleTextNode($textNode)) {
continue;
}
$texts[] = $value;
}
return $texts;
}
function extractImages(DOMXPath $xpath, DOMNode $root): array
{
$imageNodes = $xpath->query('.//img[@src]', $root);
if (!$imageNodes instanceof DOMNodeList) {
return [];
}
$images = [];
$index = 1;
foreach ($imageNodes as $imageNode) {
if (!$imageNode instanceof DOMElement) {
continue;
}
$src = trim((string) $imageNode->getAttribute('src'));
if ($src === '') {
continue;
}
$key = 'img_' . str_pad((string) $index, 3, '0', STR_PAD_LEFT);
$images[$key] = [
'src' => $src,
'alt' => normalizeText((string) $imageNode->getAttribute('alt')),
];
$index++;
}
return $images;
}
function isVisibleTextNode(DOMText $textNode): bool
{
$skipTags = [
'script', 'style', 'noscript', 'template', 'svg', 'path', 'defs',
];
$node = $textNode->parentNode;
while ($node instanceof DOMNode) {
if ($node instanceof DOMElement) {
$tag = strtolower($node->tagName);
if (in_array($tag, $skipTags, true)) {
return false;
}
}
$node = $node->parentNode;
}
return true;
}
function normalizeText(string $value): string
{
$value = str_replace(["\r", "\n", "\t"], ' ', $value);
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
return trim($value);
}
function firstNode(DOMXPath $xpath, string $query): ?DOMNode
{
$nodes = $xpath->query($query);
if (!$nodes instanceof DOMNodeList || $nodes->length === 0) {
return null;
}
$node = $nodes->item(0);
return $node instanceof DOMNode ? $node : null;
}
function firstAttrValue(DOMXPath $xpath, string $query, string $attr): string
{
$node = firstNode($xpath, $query);
if (!$node instanceof DOMElement) {
return '';
}
return normalizeText((string) $node->getAttribute($attr));
}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/add-webpage.sh" "$@"

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
PORT="${1:-4173}"
ln -sf ikfreunde.com.html website/index.html
php -d opcache.enable_cli=0 -S 127.0.0.1:"$PORT" administration/scripts/editor_server.php