Compare commits
129 Commits
ce74aa5113
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d0ba453b0 | ||
|
|
ae902c51f4 | ||
|
|
4d3f640ecd | ||
|
|
7f87efbeb7 | ||
|
|
f318440572 | ||
|
|
0747579dcf | ||
|
|
6e97805eea | ||
|
|
57739f565a | ||
|
|
b5521765a1 | ||
| e4fd13a782 | |||
| ae999e1fac | |||
| 0176cf85e3 | |||
| 907cf90b11 | |||
|
|
e1207362de | ||
|
|
5de92fa7a1 | ||
| 0129164fd5 | |||
| 75c7a76210 | |||
| 805ff80ce8 | |||
| 2b9361c7e9 | |||
| 8573e330d6 | |||
| d1364558ee | |||
| efa5d28d1d | |||
| 1f1948e40d | |||
| 0580e5121e | |||
| f1d1b8ea93 | |||
| 2e878e24f6 | |||
| 2643f19669 | |||
| 957ded280a | |||
| d701fc3adf | |||
| df07f530f6 | |||
| 09686129b9 | |||
| 1cd4e7d4ee | |||
| f8b216bea3 | |||
| 9f7e431126 | |||
| 08846aa70f | |||
| 3125f68b66 | |||
| 269d623ca9 | |||
| ca86177e90 | |||
| a126778f16 | |||
| 1b3d03cd70 | |||
| c329891e2e | |||
| 57dde1df71 | |||
| 15bda3d3d5 | |||
| 5ad5e527a7 | |||
| 7f08417eb1 | |||
| 13bc417d45 | |||
| bf1401c32b | |||
| 2ab66fc0a3 | |||
| 0416201742 | |||
| 10de61cb25 | |||
| e5960232f1 | |||
| 6c24ac7ec2 | |||
| 0914787be6 | |||
| 0121662530 | |||
| 95b6c1fa05 | |||
| 668a1ae7a3 | |||
| 71a89b7b10 | |||
| 1ff4ae2b24 | |||
| 677867dbdd | |||
| 4059d6d7be | |||
| 6d7f365069 | |||
| 8e7f86a78b | |||
| a5b560e404 | |||
| 87512d2e6d | |||
| 87efffe1c1 | |||
| 42f765366d | |||
| e392b43123 | |||
| 79eeb1b10c | |||
| 5b6f1d22b4 | |||
| 904ca07f09 | |||
| bde441326b | |||
| 3026972dee | |||
| 5dcdbd38fb | |||
| e3ef48c788 | |||
| cf60aacb0f | |||
| b80c192eba | |||
| 79b087b99c | |||
| fdad51dc6e | |||
| cb6b674522 | |||
| 323adc9d28 | |||
| d240c06364 | |||
| 71d338d6fc | |||
| 7c9dc1cd01 | |||
| 162591f527 | |||
| 4c33ada9b2 | |||
| 9a3039b011 | |||
| a1c2233358 | |||
| 494eb6c156 | |||
| c69dca47e1 | |||
| 8dacced1b4 | |||
| 306e45da5f | |||
| a6bc0707fb | |||
| bd2fef9b6c | |||
| 7d55191d91 | |||
| 6beef9fc96 | |||
| a549f3f7a6 | |||
| 0e82979a70 | |||
| c8ad950d98 | |||
| d58e5b0d12 | |||
| 18181d7ce8 | |||
|
|
74685601b2 | ||
|
|
240ee1afbf | ||
|
|
abf69e0074 | ||
|
|
e502ea69aa | ||
|
|
0b9eb0b9f0 | ||
|
|
23a22fb4d6 | ||
|
|
1b047a74fd | ||
|
|
459f51808f | ||
|
|
0b66a49d07 | ||
|
|
bc4dc6b5fb | ||
|
|
7e0d7e6466 | ||
|
|
8aeb6dbd24 | ||
|
|
7e5c31b00d | ||
|
|
af9c8f77fd | ||
|
|
47d0834c12 | ||
| b78fb42ed7 | |||
|
|
33ca681f10 | ||
|
|
f32172879c | ||
|
|
f80c529a46 | ||
| 5d6b621a99 | |||
| 8d9f714701 | |||
| b4c673bdc4 | |||
| dea59f3d23 | |||
|
|
7072e7e099 | ||
|
|
1cb9e066ab | ||
| 4e43c10b54 | |||
| 87257a0bd4 | |||
|
|
4009d49ee6 | ||
|
|
f13aa9ec7e |
@@ -1,63 +1,150 @@
|
||||
name: Build, Push, and Deploy to Nomad
|
||||
name: Build and Deploy LifeFAQ
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
docker-nomad:
|
||||
runs-on: self-hosted
|
||||
build-image:
|
||||
runs-on: debian-host
|
||||
|
||||
env:
|
||||
PATH: /usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/bin:/snap/bin
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
BUILDX_CONFIG: /tmp/buildx
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Container Registry
|
||||
run: echo ${{ secrets.password }} | docker login registry.i80.dk -u ${{ secrets.username }} --password-stdin
|
||||
|
||||
- name: Build Docker Image
|
||||
- name: System info
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||
docker build -t registry.i80.dk/gitea/lifefaq:latest -t registry.i80.dk/gitea/lifefaq:${COMMIT_HASH} .
|
||||
uname -a
|
||||
whoami
|
||||
|
||||
|
||||
- name: Push Docker Image
|
||||
- name: Set up Docker Context for Buildx
|
||||
id: buildx-context
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD)
|
||||
echo "registry.i80.dk/gitea/lifefaq:latest"
|
||||
echo "registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}"
|
||||
docker push registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}
|
||||
docker push registry.i80.dk/gitea/lifefaq:latest
|
||||
export DOCKER_HOST=tcp://docker:2376/
|
||||
export DOCKER_TLS_VERIFY=0
|
||||
docker context rm builders || true
|
||||
docker context create builders
|
||||
|
||||
- name: Verify Docker
|
||||
run: docker --version
|
||||
|
||||
- name: Validate Nomad Job
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
env:
|
||||
NOMAD_ADDR: https://nomad.i80.dk
|
||||
run: nomad job validate .gitea/workflows/nomad-job.hcl
|
||||
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
|
||||
|
||||
- name: Stop old deployment
|
||||
- name: Log in to Docker Registry
|
||||
run: |
|
||||
echo "${{ secrets.HARBOR_ROBOT_TOKEN }}" | docker login registry.i80.dk -u "robot\$gitserver" --password-stdin
|
||||
env:
|
||||
NOMAD_ADDR: https://nomad.i80.dk
|
||||
run: nomad job stop -purge -no-shutdown-delay lifefaq
|
||||
continue-on-error: true
|
||||
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v2
|
||||
with:
|
||||
filters: |
|
||||
docker:
|
||||
- 'Dockerfile'
|
||||
- 'app/**'
|
||||
- 'requirements.txt'
|
||||
|
||||
- name: Apply Nomad Job
|
||||
- name: Build and push Docker image
|
||||
if: steps.changes.outputs.docker == 'true'
|
||||
uses: docker/build-push-action@v5
|
||||
env:
|
||||
NOMAD_ADDR: https://nomad.i80.dk
|
||||
run: nomad job run .gitea/workflows/nomad-job.hcl
|
||||
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
registry.i80.dk/gitea/lifefaq:latest
|
||||
|
||||
- name: Update Nginx Configuration
|
||||
run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/nginx_updater.py lifefaq
|
||||
- name: Test container health
|
||||
run: |
|
||||
echo "=== Starting container for health check ==="
|
||||
|
||||
- name: Update Forwarder Configuration
|
||||
run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/update_forwarder.py --subdomain lifefaq
|
||||
docker pull registry.i80.dk/gitea/lifefaq:latest
|
||||
|
||||
CONTAINER_ID=$(docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e PORT=8000 \
|
||||
-e APP_ENV=production \
|
||||
--name lifefaq-test \
|
||||
registry.i80.dk/gitea/lifefaq:latest)
|
||||
|
||||
# - name: Restart Nomad Job
|
||||
# env:
|
||||
# NOMAD_ADDR: https://nomad.i80.dk
|
||||
# run: |
|
||||
# nomad job stop lifefaq
|
||||
# sleep 5 # Optional: Wait to ensure the old allocation is stopped
|
||||
# nomad job run .gitea/workflows/nomad-job.hcl
|
||||
echo "Container started: ${CONTAINER_ID}"
|
||||
|
||||
echo "Waiting for /health endpoint..."
|
||||
SUCCESS=false
|
||||
for i in {1..90}; do
|
||||
if curl -f -s http://localhost:8000/health > /dev/null 2>&1; then
|
||||
echo "✓ Health check passed after ${i} seconds"
|
||||
curl -s http://localhost:8000/health | jq '.' || echo "Health endpoint returned OK"
|
||||
SUCCESS=true
|
||||
break
|
||||
fi
|
||||
echo "Attempt ${i}/90 - waiting..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "=== Container Logs ==="
|
||||
docker logs lifefaq-test
|
||||
|
||||
docker stop lifefaq-test
|
||||
docker rm lifefaq-test
|
||||
|
||||
if [ "$SUCCESS" = false ]; then
|
||||
echo "✗ Health check failed after 90 seconds"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Container health check passed - safe to deploy"
|
||||
env:
|
||||
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
|
||||
|
||||
- name: Deploy to Nomad
|
||||
run: |
|
||||
nomad job validate lifefaq.nomad
|
||||
nomad job run lifefaq.nomad
|
||||
env:
|
||||
NOMAD_ADDR: "https://nomad.i80.dk:4646"
|
||||
|
||||
- name: Wait for deployment
|
||||
run: |
|
||||
echo "Checking deployment status..."
|
||||
nomad job status lifefaq
|
||||
|
||||
echo "=== Allocation Details ==="
|
||||
nomad job allocs lifefaq
|
||||
|
||||
echo "=== Getting logs from allocations ==="
|
||||
for alloc in $(nomad job allocs -all lifefaq | tail -n +2 | awk '{print $1}'); do
|
||||
echo "Logs for allocation $alloc:"
|
||||
|
||||
timeout=250
|
||||
SECONDS=0
|
||||
until nomad alloc logs "$alloc" 2>/dev/null || [ $SECONDS -gt $timeout ]; do
|
||||
echo "Waiting for allocation to start... ($SECONDS/$timeout seconds)"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
[ $SECONDS -gt $timeout ] && echo "Timeout for $alloc"
|
||||
echo "---"
|
||||
done
|
||||
env:
|
||||
NOMAD_ADDR: "https://nomad.i80.dk:4646"
|
||||
|
||||
- name: Notify deployment status
|
||||
run: |
|
||||
echo "✅ Deployment completed!"
|
||||
echo "LifeFAQ should be available at: https://lifefaq.i80.dk"
|
||||
echo "Health check endpoint: https://lifefaq.i80.dk/health"
|
||||
1
.gitignore
vendored
@@ -3,4 +3,5 @@
|
||||
.ídea
|
||||
.idea/*
|
||||
.gitea/**/build*/
|
||||
data/**/index.html
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ jobs:
|
||||
NOMAD_ADDR: https://nomad.i80.dk
|
||||
run: nomad job validate .gitea/workflows/nomad-job.hcl
|
||||
|
||||
- name: Stop old deployment
|
||||
env:
|
||||
NOMAD_ADDR: https://nomad.i80.dk
|
||||
run: nomad job stop -purge -no-shutdown-delay [[PROJECT_NAME]]
|
||||
continue-on-error: true
|
||||
# - name: Stop old deployment
|
||||
# env:
|
||||
# NOMAD_ADDR: https://nomad.i80.dk
|
||||
# run: nomad job stop -purge -no-shutdown-delay [[PROJECT_NAME]]
|
||||
# continue-on-error: true
|
||||
|
||||
|
||||
- name: Apply Nomad Job
|
||||
@@ -1,4 +1,4 @@
|
||||
job "lifefaq" {
|
||||
job "lifefaq-blue" {
|
||||
region = "global"
|
||||
datacenters = ["dc1"]
|
||||
type = "service"
|
||||
@@ -9,7 +9,7 @@ job "lifefaq" {
|
||||
progress_deadline = "6m"
|
||||
}
|
||||
|
||||
group "lifefaq-group" {
|
||||
group "lifefaq-blue-group" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
@@ -21,7 +21,7 @@ job "lifefaq" {
|
||||
# Register the service with Consul
|
||||
service {
|
||||
provider = "consul"
|
||||
name = "lifefaq"
|
||||
name = "lifefaq-blue"
|
||||
port = "port-app"
|
||||
|
||||
# Traefik-specific tags for routing
|
||||
@@ -38,7 +38,7 @@ job "lifefaq" {
|
||||
}
|
||||
}
|
||||
|
||||
task "lifefaq-task" {
|
||||
task "lifefaq-blue-task" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
@@ -58,3 +58,4 @@ job "lifefaq" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
Depriced/nomad-job-blue.hcl
Normal file
@@ -0,0 +1,59 @@
|
||||
job "lifefaq-blue" {
|
||||
region = "global"
|
||||
datacenters = ["dc1"]
|
||||
type = "service"
|
||||
|
||||
update {
|
||||
stagger = "60s"
|
||||
max_parallel = 1
|
||||
canary = 1
|
||||
auto_revert = true
|
||||
auto_promote = true
|
||||
progress_deadline = "6m"
|
||||
}
|
||||
|
||||
group "lifefaq-group" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
port "port-app" {
|
||||
to = 9210
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
provider = "consul"
|
||||
name = "lifefaq"
|
||||
port = "port-app"
|
||||
tags = [
|
||||
"blue",
|
||||
"PORT=${NOMAD_PORT_port-app}"
|
||||
]
|
||||
check {
|
||||
name = "tcp_check"
|
||||
type = "tcp"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
}
|
||||
}
|
||||
|
||||
task "lifefaq-task" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "registry.i80.dk/gitea/lifefaq:blue"
|
||||
ports = ["port-app"]
|
||||
}
|
||||
|
||||
env {
|
||||
APP_ENV = "production"
|
||||
PORT = "${NOMAD_PORT_port-app}"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 250
|
||||
memory = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Depriced/nomad-job-canary.hcl
Normal file
@@ -0,0 +1,59 @@
|
||||
job "lifefaq-canary" {
|
||||
region = "global"
|
||||
datacenters = ["dc1"]
|
||||
type = "service"
|
||||
|
||||
update {
|
||||
stagger = "60s"
|
||||
max_parallel = 1
|
||||
canary = 1
|
||||
auto_revert = true
|
||||
auto_promote = true
|
||||
progress_deadline = "6m"
|
||||
}
|
||||
|
||||
group "lifefaq-group" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
port "port-app" {
|
||||
to = 9210
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
provider = "consul"
|
||||
name = "lifefaq"
|
||||
port = "port-app"
|
||||
tags = [
|
||||
"canary",
|
||||
"PORT=${NOMAD_PORT_port-app}"
|
||||
]
|
||||
check {
|
||||
name = "tcp_check"
|
||||
type = "tcp"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
}
|
||||
}
|
||||
|
||||
task "lifefaq-task" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}"
|
||||
ports = ["port-app"]
|
||||
}
|
||||
|
||||
env {
|
||||
APP_ENV = "production"
|
||||
PORT = "${NOMAD_PORT_port-app}"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 250
|
||||
memory = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Depriced/nomad-job-green.hcl
Normal file
@@ -0,0 +1,59 @@
|
||||
job "lifefaq-green" {
|
||||
region = "global"
|
||||
datacenters = ["dc1"]
|
||||
type = "service"
|
||||
|
||||
update {
|
||||
stagger = "60s"
|
||||
max_parallel = 1
|
||||
canary = 1
|
||||
auto_revert = true
|
||||
auto_promote = true
|
||||
progress_deadline = "6m"
|
||||
}
|
||||
|
||||
group "lifefaq-group" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
port "port-app" {
|
||||
to = 9210
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
provider = "consul"
|
||||
name = "lifefaq"
|
||||
port = "port-app"
|
||||
tags = [
|
||||
"green",
|
||||
"PORT=${NOMAD_PORT_port-app}"
|
||||
]
|
||||
check {
|
||||
name = "tcp_check"
|
||||
type = "tcp"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
}
|
||||
}
|
||||
|
||||
task "lifefaq-task" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "registry.i80.dk/gitea/lifefaq:green"
|
||||
ports = ["port-app"]
|
||||
}
|
||||
|
||||
env {
|
||||
APP_ENV = "production"
|
||||
PORT = "${NOMAD_PORT_port-app}"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 250
|
||||
memory = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Depriced/waypoint.hcl
Normal file
@@ -0,0 +1,81 @@
|
||||
project = "lifefaq"
|
||||
|
||||
app "lifefaq" {
|
||||
build {
|
||||
use "docker" {
|
||||
image = "registry.i80.dk/gitea/lifefaq:latest"
|
||||
}
|
||||
}
|
||||
|
||||
deploy {
|
||||
use "nomad" {
|
||||
job = <<EOT
|
||||
job "lifefaq" {
|
||||
region = "global"
|
||||
datacenters = ["dc1"]
|
||||
type = "service"
|
||||
|
||||
update {
|
||||
stagger = "60s"
|
||||
max_parallel = 1
|
||||
canary = 1
|
||||
auto_revert = true
|
||||
auto_promote = true
|
||||
progress_deadline = "6m"
|
||||
}
|
||||
|
||||
group "lifefaq-group" {
|
||||
count = 1
|
||||
|
||||
network {
|
||||
port "port-app" {
|
||||
to = 9210 # Internal application port
|
||||
}
|
||||
}
|
||||
|
||||
service {
|
||||
provider = "consul"
|
||||
name = "lifefaq"
|
||||
port = "port-app"
|
||||
|
||||
tags = [
|
||||
"PORT=${NOMAD_PORT_port-app}"
|
||||
]
|
||||
|
||||
check {
|
||||
name = "tcp_check"
|
||||
type = "tcp"
|
||||
interval = "10s"
|
||||
timeout = "2s"
|
||||
}
|
||||
}
|
||||
|
||||
task "lifefaq-task" {
|
||||
driver = "docker"
|
||||
|
||||
config {
|
||||
image = "registry.i80.dk/gitea/lifefaq:latest"
|
||||
ports = ["port-app"]
|
||||
}
|
||||
|
||||
env {
|
||||
APP_ENV = "production"
|
||||
PORT = "${NOMAD_PORT_port-app}"
|
||||
}
|
||||
|
||||
resources {
|
||||
cpu = 250
|
||||
memory = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
release {
|
||||
use "nomad" {
|
||||
strategy = "bluegreen"
|
||||
}
|
||||
}
|
||||
13
Dockerfile
@@ -1,20 +1,13 @@
|
||||
# Base image with Python 3.11
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file to the working directory
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Expose the port the FastAPI app runs on (default Uvicorn port)
|
||||
EXPOSE 9210
|
||||
# Port will be set via environment variable
|
||||
EXPOSE 8000
|
||||
|
||||
# Command to run the FastAPI application
|
||||
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "9210", "--workers", "1"]
|
||||
CMD ["sh", "-c", "uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port ${PORT:-8000} --workers 1"]
|
||||
|
||||
BIN
FlyverPriser.ods
Normal file
47
Makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
.PHONY := help install run start stop docker-build docker-rebuild docker-run docker-stop docker-logs docker-shell clean
|
||||
|
||||
PROJECT_NAME ?= lifefaq
|
||||
IMAGE_NAME ?= $(PROJECT_NAME):latest
|
||||
CONTAINER_NAME ?= $(PROJECT_NAME)-app
|
||||
PYTHON ?= python3
|
||||
UVICORN ?= uvicorn
|
||||
APP_MODULE ?= app.main:app
|
||||
PORT ?= 8000
|
||||
HOST_PORT ?= $(PORT)
|
||||
|
||||
help:
|
||||
@printf "Available targets:\n"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "} {printf " %-18s %s\n", $$1, $$2}' | sort
|
||||
|
||||
install: ## Install Python dependencies
|
||||
$(PYTHON) -m pip install -r requirements.txt
|
||||
|
||||
run: ## Run the FastAPI app using app.py (no auto-reload)
|
||||
$(PYTHON) app.py
|
||||
|
||||
start: ## Start the FastAPI app with uvicorn auto-reload (foreground)
|
||||
$(UVICORN) $(APP_MODULE) --reload --host 0.0.0.0 --port $(PORT)
|
||||
|
||||
stop: ## Stop local uvicorn processes started via make start (best effort)
|
||||
-pkill -f "$(UVICORN).*$(APP_MODULE)"
|
||||
|
||||
docker-build: ## Build the Docker image
|
||||
docker build -t $(IMAGE_NAME) .
|
||||
|
||||
docker-rebuild: ## Rebuild the Docker image without cache
|
||||
docker build --no-cache -t $(IMAGE_NAME) .
|
||||
|
||||
docker-run: ## Run the Docker container in the background
|
||||
docker run --rm -d -p $(HOST_PORT):$(PORT) --name $(CONTAINER_NAME) -e PORT=$(PORT) $(IMAGE_NAME)
|
||||
|
||||
docker-stop: ## Stop the running Docker container
|
||||
-docker stop $(CONTAINER_NAME)
|
||||
|
||||
docker-logs: ## Tail logs from the Docker container
|
||||
docker logs -f $(CONTAINER_NAME)
|
||||
|
||||
docker-shell: ## Open a shell inside the running Docker container
|
||||
docker exec -it $(CONTAINER_NAME) /bin/sh
|
||||
|
||||
clean: ## Remove Python cache artifacts
|
||||
find . -name '__pycache__' -type d -prune -exec rm -rf {} +
|
||||
BIN
PortugalBudget.ods
Normal file
BIN
__pycache__/markdown_render.cpython-311.pyc
Normal file
9
app.py
@@ -1,5 +1,12 @@
|
||||
import os
|
||||
import uvicorn
|
||||
from app.main import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=9210, reload=False)
|
||||
port = int(os.getenv("PORT", 8000))
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=port,
|
||||
reload=False
|
||||
)
|
||||
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/__pycache__/main.cpython-311.pyc
Normal file
BIN
app/controllers/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/controllers/__pycache__/category_controller.cpython-311.pyc
Normal file
BIN
app/controllers/__pycache__/dynamic_controller.cpython-311.pyc
Normal file
BIN
app/controllers/__pycache__/route_to_web.cpython-311.pyc
Normal file
BIN
app/controllers/__pycache__/route_to_web.cpython-312.pyc
Normal file
@@ -3,6 +3,10 @@ import json
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import JSONResponse
|
||||
import time
|
||||
|
||||
from app.services.metadata_processor import MetadataProcessor
|
||||
|
||||
|
||||
class CategoryController:
|
||||
@@ -22,21 +26,76 @@ class CategoryController:
|
||||
methods=["GET"],
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/categories", self.list_categories, methods = ["GET"], response_class = JSONResponse
|
||||
)
|
||||
|
||||
def _load_data(self, data_file):
|
||||
"""Load JSON data from a file."""
|
||||
"""Load JSON data from a file. If the file is missing, generate it."""
|
||||
if not os.path.exists(data_file):
|
||||
print(f"{data_file} not found. Generating JSON...")
|
||||
self.generate_json() # Call the JSON generation method
|
||||
with open(data_file, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
|
||||
async def get_index(self, request: Request):
|
||||
"""Index route."""
|
||||
"""
|
||||
Handle requests for the index (home) page.
|
||||
|
||||
This function is executed every time the root route (index page) is accessed.
|
||||
It renders the 'index.html' template and populates it with dynamic data, such as:
|
||||
- 'page_title': A static title for the home page ("Forside").
|
||||
- 'author': The author's name ("Henrik").
|
||||
- 'data': General data accessible to the template.
|
||||
|
||||
Args:
|
||||
request (Request): The HTTP request object.
|
||||
|
||||
Returns:
|
||||
TemplateResponse: A rendered HTML page for the index (home) route.
|
||||
"""
|
||||
|
||||
unix_time_now = int( time.time() )
|
||||
with open(f"data/_frontpage/index.html", "r") as fp:
|
||||
content = fp.read()
|
||||
|
||||
return self.templates.TemplateResponse(
|
||||
"index.html",
|
||||
{"request": request, "data": self.data, "page_title": "Forside", "author": "Henrik"},
|
||||
)
|
||||
"category.html",
|
||||
{
|
||||
"request": request,
|
||||
"data": self.data,
|
||||
"page_title": "Frontpage",
|
||||
"author": "Henrik Jess",
|
||||
"content": content,
|
||||
"timestamp": unix_time_now
|
||||
})
|
||||
|
||||
|
||||
async def get_category(self, request: Request, category_name: str):
|
||||
"""Category route."""
|
||||
"""
|
||||
Handle requests for specific category pages.
|
||||
|
||||
This function is executed every time a category route is accessed.
|
||||
It dynamically retrieves and serves content for the requested category.
|
||||
- Searches for the requested category in 'self.data["categories"]' based on the provided category name.
|
||||
- Reads the 'index.html' file located under 'data/{category_name}/' if it exists.
|
||||
- Returns the rendered 'category.html' template with the following dynamic data:
|
||||
- 'page_title': The name of the category.
|
||||
- 'author': The author of the category.
|
||||
- 'content': The content of the 'index.html' file.
|
||||
- 'timestamp': The current Unix time when the request is processed.
|
||||
- Returns a 404 HTML response if the category is not found or the file does not exist.
|
||||
|
||||
Args:
|
||||
request (Request): The HTTP request object.
|
||||
category_name (str): The name of the category being accessed.
|
||||
|
||||
Returns:
|
||||
TemplateResponse: A rendered HTML page with dynamic category content.
|
||||
HTMLResponse: A 404 response if the category does not exist.
|
||||
"""
|
||||
category = next((cat for cat in self.data["categories"] if cat["path"] == category_name), None)
|
||||
unix_time_now = int( time.time() )
|
||||
if category:
|
||||
category_file = f"data/{category_name}/index.html"
|
||||
if os.path.exists(category_file):
|
||||
@@ -50,6 +109,15 @@ class CategoryController:
|
||||
"page_title": category["name"],
|
||||
"author": category["author"],
|
||||
"content": category_content,
|
||||
"timestamp": unix_time_now
|
||||
},
|
||||
)
|
||||
return HTMLResponse("Kategori ikke fundet", status_code=404)
|
||||
|
||||
async def list_categories(self, request: Request):
|
||||
"""Return a list of all categories with their name and path."""
|
||||
categories = [
|
||||
{ "name": category["name"], "path": category["path"] }
|
||||
for category in self.data.get( "categories", [] )
|
||||
]
|
||||
return JSONResponse( content = categories )
|
||||
@@ -61,5 +61,4 @@ class DynamicController:
|
||||
|
||||
# Fallback: Return a 404 if no content is found
|
||||
return Response(f"No content found for {route_name}", status_code=404)
|
||||
|
||||
return route_handler
|
||||
|
||||
86
app/controllers/route_to_web.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
import random
|
||||
|
||||
from fastapi import APIRouter, Request, FastAPI
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from app.controllers.category_controller import CategoryController
|
||||
|
||||
class RouteToWeb:
|
||||
def __init__(self, app: FastAPI):
|
||||
"""Initialize the controller."""
|
||||
self.router = APIRouter()
|
||||
self.templates = Jinja2Templates(directory="templates")
|
||||
self.app = app
|
||||
self.category_controller = CategoryController()
|
||||
self._add_routes()
|
||||
self._add_global_middleware()
|
||||
|
||||
def _add_routes(self):
|
||||
"""Add routes to the router."""
|
||||
@self.router.get("/route-list", tags=["system"])
|
||||
async def route_list(request: Request):
|
||||
"""Render route list with categories."""
|
||||
routes = [
|
||||
{"path": route.path, "name": route.name or "Unnamed"}
|
||||
for route in self.app.routes
|
||||
]
|
||||
categories = request.state.categories
|
||||
return self.templates.TemplateResponse(
|
||||
"route_list.html",
|
||||
{"request": request, "routes": routes, "categories": categories, "page_title": "Route og Kategori Liste"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _add_global_middleware(self):
|
||||
"""Middleware to add categories and next category globally to all requests."""
|
||||
|
||||
@self.app.middleware( "http" )
|
||||
async def add_categories_to_request(request: Request, call_next):
|
||||
def generate_dynamic_description(category_name: str) -> str:
|
||||
"""Generate a dynamic and engaging link text for a category."""
|
||||
templates = [
|
||||
"Dyk ned i kategorien {category} og bliv inspireret!",
|
||||
"Opdag alt, hvad du behøver at vide i kategorien {category}.",
|
||||
"Udforsk {category}-kategorien og find noget nyt og spændende.",
|
||||
"Lad dig fordybe i kategorien {category} – der er meget at se!",
|
||||
"Find din næste læseoplevelse i {category}-kategorien.",
|
||||
"Gå på opdagelse i kategorien {category} og bliv klogere.",
|
||||
"Der venter spændende indhold i {category}-kategorien – klik her!",
|
||||
"Vil du vide mere? Hele kategorien {category} er kun ét klik væk.",
|
||||
"Læs videre i kategorien {category} og få ny inspiration.",
|
||||
"Fordyb dig i {category}-kategorien og opdag nyt indhold.",
|
||||
"Spring ind i {category}-kategorien og gå på opdagelse!",
|
||||
"Find masser af viden og gode læseoplevelser i {category}-kategorien.",
|
||||
"Udforsk hele kategorien {category} og bliv beriget med ny viden.",
|
||||
"Der er mere at læse i {category}-kategorien – gå ikke glip af det!",
|
||||
"Tag et dybere kig i kategorien {category} og bliv inspireret!"
|
||||
]
|
||||
template = random.choice( templates )
|
||||
return template.format( category = category_name.lower() )
|
||||
"""Inject categories and next category into request.state globally."""
|
||||
# Hent kategorier direkte fra CategoryController
|
||||
categories_response = await self.category_controller.list_categories( request )
|
||||
categories_data = categories_response.body.decode()
|
||||
categories = json.loads( categories_data )
|
||||
|
||||
# Tilføj kategorier til request.state
|
||||
request.state.categories = categories
|
||||
|
||||
# Find den aktuelle og næste kategori
|
||||
current_path = request.url.path.split("/")[-1]
|
||||
next_category = None
|
||||
for index, category in enumerate( categories ):
|
||||
if category["path"] == current_path:
|
||||
# Find næste kategori (cirkulær, hvis det er den sidste)
|
||||
next_index = (index + 1) % len( categories )
|
||||
next_category = categories[next_index]
|
||||
next_category["description"] = generate_dynamic_description( next_category["path"] )
|
||||
|
||||
break
|
||||
|
||||
# Tilføj næste kategori til request.state
|
||||
request.state.next_category = next_category
|
||||
|
||||
response = await call_next( request )
|
||||
return response
|
||||
45
app/main.py
@@ -1,35 +1,40 @@
|
||||
from fastapi import FastAPI
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import app
|
||||
from app.controllers.route_to_web import RouteToWeb
|
||||
from app.services.markdown_processor import MarkdownProcessor
|
||||
from app.services.metadata_processor import MetadataProcessor
|
||||
from app.controllers.dynamic_controller import DynamicController
|
||||
from app.controllers.category_controller import CategoryController
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from app.services.image_service import ImageService
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self):
|
||||
"""Initialize the FastAPI app and configure it."""
|
||||
self.app = FastAPI( lifespan = self._lifespan_event )
|
||||
self._set_image_sizes()
|
||||
self._setup_static_files()
|
||||
self._setup_health_route()
|
||||
self._include_routers()
|
||||
self._include_middelware()
|
||||
|
||||
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _lifespan_event(self, app: FastAPI):
|
||||
"""Lifespan event for startup and shutdown logic."""
|
||||
print("App startup: Processing Markdown files...")
|
||||
# Generate dynamic JSON data
|
||||
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json")
|
||||
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json",app=self.app)
|
||||
metadata_processor.generate_json()
|
||||
print("Generated dynamic data file.")
|
||||
print("Markdown processing complete!")
|
||||
|
||||
# Process Markdown files into HTML
|
||||
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates")
|
||||
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates",app=self.app)
|
||||
processor.run()
|
||||
|
||||
|
||||
|
||||
yield
|
||||
print("App shutdown: Cleanup complete.")
|
||||
|
||||
@@ -37,14 +42,34 @@ class Application:
|
||||
"""Mount static file directories."""
|
||||
self.app.mount("/data", StaticFiles(directory="data"), name="data")
|
||||
self.app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
self.app.mount( "/images", StaticFiles( directory = "static/images" ), name = "images" )
|
||||
|
||||
def _include_routers(self):
|
||||
"""Include all route controllers."""
|
||||
category_controller = CategoryController()
|
||||
dynamic_controller = DynamicController("./data")
|
||||
image_service = ImageService(self.app)
|
||||
route_to_web = RouteToWeb(self.app)
|
||||
|
||||
self.app.include_router( category_controller.router )
|
||||
self.app.include_router(dynamic_controller.router)
|
||||
self.app.include_router(route_to_web.router)
|
||||
self.app.include_router( image_service.router )
|
||||
|
||||
def _setup_health_route(self):
|
||||
@self.app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
def _include_middelware(self):
|
||||
self.app.add_middleware( GZipMiddleware, minimum_size = 500 )
|
||||
|
||||
def _set_image_sizes(self):
|
||||
self.app.state.IMAGE_SIZES = {
|
||||
'thumbnails': {'width': 150, 'height': 150},
|
||||
'large': {'width': 800, 'height': 600},
|
||||
'small': {'width': 300, 'height': 300},
|
||||
'original': {'width': None, 'height': None}, # Original størrelse
|
||||
}
|
||||
|
||||
def get_app(self):
|
||||
"""Return the FastAPI app instance."""
|
||||
|
||||
BIN
app/services/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/services/__pycache__/image_service.cpython-311.pyc
Normal file
BIN
app/services/__pycache__/image_service.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/markdown_processor.cpython-311.pyc
Normal file
BIN
app/services/__pycache__/markdown_render.cpython-311.pyc
Normal file
BIN
app/services/__pycache__/metadata_processor.cpython-311.pyc
Normal file
180
app/services/image_service.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi import APIRouter, Request, FastAPI
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class FileHandler:
|
||||
|
||||
def __init__(self, category=None, image_type=None, filename=None) -> Path:
|
||||
self.filename = filename
|
||||
self.category = category
|
||||
self.image_type = image_type
|
||||
|
||||
@property
|
||||
def src_file(self) -> str:
|
||||
src_path = "data/{category}/images/{filename}"
|
||||
return src_path.format( category = self.category, filename = self.filename )
|
||||
|
||||
@property
|
||||
def dest_file(self) -> str:
|
||||
base_url = "/images/{category}/{filename}"
|
||||
return base_url.format( category = self.category, filename = self.filename )
|
||||
|
||||
@property
|
||||
def dest_filename(self) -> str:
|
||||
base_url = "static/images/{category}/{image_type}/{filename}"
|
||||
return base_url.format( category = self.category, image_type = self.image_type, filename = self.filename )
|
||||
@property
|
||||
def dest_filename_webp(self) -> str:
|
||||
base_url = "static/images/{category}/{image_type}/{filename}"
|
||||
path = Path( base_url.format( category = self.category, image_type = self.image_type, filename = self.filename ) )
|
||||
if path.suffix != ".webp":
|
||||
path = path.with_suffix( ".webp" )
|
||||
return str(path)
|
||||
|
||||
@property
|
||||
def dest_path(self) -> str:
|
||||
base_url = "static/images/{category}/{image_type}"
|
||||
return base_url.format( category = self.category, image_type = self.image_type )
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"FileHandler(\n"
|
||||
f" filename='{self.filename}',\n"
|
||||
f" category='{self.category}',\n"
|
||||
f" image_type='{self.image_type}',\n"
|
||||
f" src_file='{self.src_file}',\n"
|
||||
f" dest_file='{self.dest_file}',\n"
|
||||
f" dest_filename='{self.dest_filename}',\n"
|
||||
f" dest_path='{self.dest_path}'\n"
|
||||
f")"
|
||||
)
|
||||
|
||||
def get_category(self, file_path):
|
||||
# List all categories in the data directory
|
||||
categories = [
|
||||
name for name in os.listdir( "data/" )
|
||||
if os.path.isdir( os.path.join( "data/", name ) )
|
||||
]
|
||||
|
||||
# Search for the category in the file path
|
||||
for category in categories:
|
||||
if f"/{category}/" in file_path or f"\\{category}\\" in file_path:
|
||||
return category
|
||||
|
||||
# Return None if no category matches
|
||||
return None
|
||||
|
||||
|
||||
class ImageService:
|
||||
|
||||
def __init__(self,app: FastAPI):
|
||||
self.router = APIRouter()
|
||||
self.app = app
|
||||
self.IMAGE_SIZES = self.app.state.IMAGE_SIZES
|
||||
#self._ensure_directories_exist()
|
||||
self._add_routes()
|
||||
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Provides a string representation of the class instance.
|
||||
"""
|
||||
base_paths_str = "\n".join(
|
||||
[f"{key}: {value}" for key, value in self.base_paths.items()]
|
||||
)
|
||||
image_sizes_str = "\n".join(
|
||||
[
|
||||
f"{key}: width={value['width']}, height={value['height']}"
|
||||
for key, value in self.image_sizes.items()
|
||||
]
|
||||
)
|
||||
return f"<Class:ImageService Base Paths:{base_paths_str} Image Sizes:\n{image_sizes_str}"
|
||||
|
||||
def get_image_size(self, image_type: str) -> dict:
|
||||
"""
|
||||
Retrieve the width and height for a given image type from the app state.
|
||||
|
||||
Args:
|
||||
request (Request): FastAPI request object.
|
||||
image_type (str): The type of the image (e.g., 'thumbnails').
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with 'width' and 'height'.
|
||||
"""
|
||||
|
||||
image_sizes = self.app.state.IMAGE_SIZES
|
||||
if image_type not in image_sizes:
|
||||
raise ValueError( f"Invalid image type: {image_type}. Must be one of {list( image_sizes.keys() )}" )
|
||||
|
||||
return image_sizes[image_type]
|
||||
|
||||
def _add_routes(self):
|
||||
self.router.add_api_route(
|
||||
"/image/{category}/{type}/{filename}",
|
||||
self.get_image,
|
||||
methods=["GET"],
|
||||
response_class=FileResponse,
|
||||
)
|
||||
|
||||
async def get_image(self, category: str, type: str, filename: str):
|
||||
"""
|
||||
Retrieve an image file from the specified category and type.
|
||||
"""
|
||||
file_path = self._resolve_path(category, type, filename)
|
||||
return FileResponse(file_path)
|
||||
|
||||
def validate_image(self, file_path:FileHandler=None, width:int=None, height:int=None, overwrite = True ) -> bool:
|
||||
if not os.path.exists( file_path.dest_filename ):
|
||||
with Image.open( file_path.src_file ) as img:
|
||||
print(file_path.src_file)
|
||||
self._resize_image( img, file_path, width, height )
|
||||
return True
|
||||
|
||||
with Image.open( file_path.dest_filename ) as img:
|
||||
if img.width != width or img.height != height:
|
||||
if overwrite:
|
||||
self._resize_image( img, file_path, width, height )
|
||||
return False
|
||||
return True
|
||||
|
||||
def _resize_image(self, img: Image.Image, file_path: FileHandler, width: int, height: int):
|
||||
resized_img = img.resize( (width, height), Image.Resampling.LANCZOS )
|
||||
os.makedirs(file_path.dest_path,exist_ok = True)
|
||||
resized_img.save( file_path.dest_filename )
|
||||
resized_img.save( file_path.dest_filename_webp, format = "WEBP", quality = 90 ) # Adjust quality as needed
|
||||
print(file_path.dest_filename_webp)
|
||||
|
||||
async def get_image(self, category: str, type: str, filename: str):
|
||||
file_path = self._resolve_path( category, type, filename )
|
||||
return FileResponse( file_path )
|
||||
|
||||
def image_tag(self, category: str, image_type: str, filename: str, alt: str = "", width: int = None,
|
||||
height: int = None,css_class:str=None) -> str:
|
||||
"""
|
||||
Generate an HTML <img> tag with default sizes if dimensions are not provided.
|
||||
"""
|
||||
# Use default sizes if none are provided
|
||||
default_size = self.get_image_size( image_type)
|
||||
width = width or default_size.get( "width" )
|
||||
height = height or default_size.get( "height" )
|
||||
file_path = FileHandler(category = category,image_type = image_type,filename = filename)
|
||||
p = Path(file_path.dest_path)
|
||||
p.mkdir(parents = True, exist_ok = True)
|
||||
self.validate_image( file_path, width = width,height=height, overwrite = True )
|
||||
tag = f'<img src="/{file_path.dest_filename_webp}" alt="{alt}"'
|
||||
# if width:
|
||||
# tag += f' width="{width}"'
|
||||
# if height:
|
||||
# tag += f' height="{height}"'
|
||||
if css_class:
|
||||
tag += f' class="{css_class}"'
|
||||
tag += ">"
|
||||
|
||||
return tag
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
from app.services.markdown_render import render_markdown_with_jinja # Your custom renderer
|
||||
from bs4 import BeautifulSoup
|
||||
from fastapi import FastAPI
|
||||
from app.services.markdown_render import MarkdownRenderer
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
@@ -9,7 +11,7 @@ class MarkdownProcessor:
|
||||
'index.html' per category directory using a custom rendering engine.
|
||||
"""
|
||||
|
||||
def __init__(self, input_dir: str, templates_dir: str):
|
||||
def __init__(self, input_dir: str, templates_dir: str,app:FastAPI=None):
|
||||
"""
|
||||
Initialize the MarkdownProcessor.
|
||||
|
||||
@@ -19,6 +21,8 @@ class MarkdownProcessor:
|
||||
"""
|
||||
self.input_dir = input_dir
|
||||
self.env = Environment(loader=FileSystemLoader(templates_dir))
|
||||
self.app = app
|
||||
|
||||
|
||||
def _process_markdown_files_in_directory(self, directory_path: str) -> list:
|
||||
"""
|
||||
@@ -30,15 +34,17 @@ class MarkdownProcessor:
|
||||
Returns:
|
||||
list: A list of processed sections containing metadata and rendered content.
|
||||
"""
|
||||
from pathlib import Path
|
||||
sections = []
|
||||
for file in sorted(os.listdir(directory_path)):
|
||||
if file.endswith(".md"):
|
||||
file_path = os.path.join(directory_path, file)
|
||||
with open(file_path, "r", encoding="utf-8") as md_file:
|
||||
markdown_content = md_file.read()
|
||||
|
||||
markdown_render = MarkdownRenderer(file_path=file_path,app=self.app)
|
||||
# Process Markdown and Jinja2
|
||||
rendered_content, metadata = render_markdown_with_jinja(markdown_content)
|
||||
|
||||
rendered_content, metadata = markdown_render.render_markdown_with_jinja ( markdown_content )
|
||||
|
||||
# Append the section to the list
|
||||
sections.append({
|
||||
@@ -69,7 +75,9 @@ class MarkdownProcessor:
|
||||
# Write the rendered HTML to index.html
|
||||
os.makedirs(directory_path, exist_ok=True)
|
||||
with open(output_file, "w", encoding="utf-8") as output:
|
||||
output.write(rendered_html)
|
||||
soup = BeautifulSoup( rendered_html, 'html.parser' )
|
||||
cleaned_html = soup.prettify(formatter="html5")
|
||||
output.write(cleaned_html)
|
||||
print(f"Generated: {output_file}")
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import markdown
|
||||
from fastapi import FastAPI
|
||||
from jinja2 import Environment, DictLoader
|
||||
from markupsafe import Markup
|
||||
from .image_service import ImageService, FileHandler
|
||||
|
||||
# Define Jinja2 custom functions
|
||||
def img_left_overlay(src):
|
||||
|
||||
class MarkdownRenderer:
|
||||
def __init__(self, file_path: str = None, app: FastAPI=None):
|
||||
"""
|
||||
Initialize the MarkdownRenderer with a Jinja2 environment and custom functions.
|
||||
"""
|
||||
self.app = app
|
||||
self.image_service = ImageService(self.app)
|
||||
self.jinja_env = self._create_jinja_environment()
|
||||
self.file_path = file_path
|
||||
|
||||
|
||||
|
||||
def _create_jinja_environment(self) -> Environment:
|
||||
"""
|
||||
Create and configure the Jinja2 environment with custom functions.
|
||||
|
||||
Returns:
|
||||
Environment: A configured Jinja2 environment.
|
||||
"""
|
||||
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
|
||||
|
||||
env.globals.update({
|
||||
"img_left_overlay": self.img_left_overlay,
|
||||
"box": self.box,
|
||||
"note": self.note,
|
||||
"warning": self.warning,
|
||||
"link_to": self.link_to,
|
||||
"slider": self.slider,
|
||||
"image": self.get_image, # Add image handler function
|
||||
})
|
||||
return env
|
||||
|
||||
def img_left_overlay(self, src: str) -> str:
|
||||
"""Render an image with overlay."""
|
||||
return f'''
|
||||
<div class="img-left-overlay">
|
||||
<img src="{src}" alt="Overlay Image">
|
||||
<img src="{src}" alt="Overlay Image" loading="lazy">
|
||||
<div class="overlay-text">Overlay Text</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
def box(title, content):
|
||||
def box(self, title: str, content: str) -> str:
|
||||
"""Render a box component."""
|
||||
return f'''
|
||||
<div class="box">
|
||||
@@ -20,20 +57,21 @@ def box(title, content):
|
||||
</div>
|
||||
'''
|
||||
|
||||
def note(content):
|
||||
def note(self, content: str) -> str:
|
||||
"""Render a note component."""
|
||||
return f'''
|
||||
<div class="note">
|
||||
<p>{content}</p>
|
||||
</div>
|
||||
'''
|
||||
def link_to(title, url):
|
||||
"""Render a box component."""
|
||||
|
||||
def link_to(self, title: str, url: str) -> str:
|
||||
"""Render a link component."""
|
||||
return f'''
|
||||
<a href="{url}" target="_blank" rel="noopener noreferrer">{title}</a>
|
||||
'''
|
||||
|
||||
def warning(content):
|
||||
def warning(self, content: str) -> str:
|
||||
"""Render a warning component."""
|
||||
return f'''
|
||||
<div class="warning">
|
||||
@@ -41,19 +79,75 @@ def warning(content):
|
||||
</div>
|
||||
'''
|
||||
|
||||
def create_jinja_environment():
|
||||
"""Create and configure the Jinja2 environment."""
|
||||
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
|
||||
env.globals.update({
|
||||
"img_left_overlay": img_left_overlay,
|
||||
"box": box,
|
||||
"note": note,
|
||||
"warning": warning,
|
||||
"link_to": link_to,
|
||||
})
|
||||
return env
|
||||
def slider(self, options: dict, images: list) -> str:
|
||||
"""Render a slider component."""
|
||||
import uuid
|
||||
modal_id = uuid.uuid4().hex.upper()[0:6]
|
||||
|
||||
def render_markdown_with_jinja(markdown_content: str):
|
||||
|
||||
html_content = []
|
||||
html_content.append('<div class="button-stack">')
|
||||
for i, val in enumerate(images):
|
||||
self.image_service = ImageService( self.app )
|
||||
modal_id_current = f"{modal_id}_{i}"
|
||||
modal_id_next = f"{modal_id}_{i + 1}" if i + 1 < len(images) else f"{modal_id}_0"
|
||||
|
||||
category = FileHandler().get_category(self.file_path)
|
||||
|
||||
thumbnal_img = self.image_service.image_tag(category = category, image_type = "thumbnails",filename = val,alt="A better description later on")
|
||||
modal_img = self.image_service.image_tag(category = category, image_type = "large",filename = val,alt="A better description later on")
|
||||
html_content.append(f"""
|
||||
<button onclick="openModal('modal{modal_id_current}')" class="stacked-button">
|
||||
{thumbnal_img}
|
||||
</button>
|
||||
<div class="modal" id="modal{modal_id_current}">
|
||||
<div class="modal-content">
|
||||
<h2>Modal {i}</h2>
|
||||
{modal_img}
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button onclick="closeModal('modal{modal_id_current}')">Close</button>
|
||||
<button class="next-btn" onclick="nextModal('modal{modal_id_current}', 'modal{modal_id_next}')">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""")
|
||||
html_content.append('</div>')
|
||||
return '\n'.join(html_content)
|
||||
|
||||
def _get_category(self):
|
||||
if isinstance(self.file_path, str):
|
||||
this_path = Path(self.file_path)
|
||||
return this_path.parent.name
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_image(self, image_type: str, filename: str, alt: str = "", width: int = None, height: int = None,css_class:str=None) -> Markup:
|
||||
"""
|
||||
Generate a dynamic HTML <img> tag for an image using ImageService's image_tag method.
|
||||
"""
|
||||
valid_types = ['thumbnails', 'large', 'small', 'original']
|
||||
if image_type not in valid_types:
|
||||
sys.tracebacklimit = 0
|
||||
raise ValueError( f"Invalid image type: {image_type}. Must be one of {valid_types}" )
|
||||
|
||||
tag = self.image_service.image_tag(
|
||||
category=self._get_category(),
|
||||
image_type=image_type,
|
||||
filename=filename,
|
||||
alt=alt,
|
||||
width=width,
|
||||
height=height,
|
||||
css_class=css_class
|
||||
)
|
||||
return Markup(tag)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def render_markdown_with_jinja(self, markdown_content: str):
|
||||
"""
|
||||
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
|
||||
|
||||
@@ -63,17 +157,16 @@ def render_markdown_with_jinja(markdown_content: str):
|
||||
Returns:
|
||||
tuple: Rendered HTML content and metadata as a dictionary.
|
||||
"""
|
||||
# Step 1: Convert Markdown to HTML and extract metadata
|
||||
|
||||
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
|
||||
intermediate_html = md.convert(markdown_content)
|
||||
metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
|
||||
|
||||
# Step 2: Pass the resulting HTML with Jinja2 custom tags through Jinja2
|
||||
env = create_jinja_environment()
|
||||
template = env.get_template("base_template")
|
||||
# Step 3: Pass the resulting HTML with Jinja2 custom tags through Jinja2
|
||||
template = self.jinja_env.get_template("base_template")
|
||||
final_html = template.render(content=intermediate_html)
|
||||
|
||||
# Step 3: Re-render final_html in Jinja2 for embedded tags like {{ box(...) }}
|
||||
final_output = env.from_string(final_html).render()
|
||||
# Step 4: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
|
||||
final_output = self.jinja_env.from_string(final_html).render()
|
||||
|
||||
return final_output, metadata
|
||||
@@ -3,6 +3,10 @@ import markdown
|
||||
import json
|
||||
from typing import List, Dict
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.services.image_service import ImageService, FileHandler
|
||||
|
||||
|
||||
class MetadataProcessor:
|
||||
"""
|
||||
@@ -10,7 +14,7 @@ class MetadataProcessor:
|
||||
and generate a structured JSON file.
|
||||
"""
|
||||
|
||||
def __init__(self, input_dir: str, output_file: str):
|
||||
def __init__(self, input_dir: str, output_file: str,app:FastAPI=None):
|
||||
"""
|
||||
Initialize the MetadataProcessor.
|
||||
|
||||
@@ -20,6 +24,7 @@ class MetadataProcessor:
|
||||
"""
|
||||
self.input_dir = input_dir
|
||||
self.output_file = output_file
|
||||
self.app = app
|
||||
self.data = {"categories": [], "favorites": []}
|
||||
|
||||
def _extract_metadata(self, file_path: str) -> Dict:
|
||||
@@ -64,11 +69,25 @@ class MetadataProcessor:
|
||||
|
||||
# Add to 'favorites' if 'favorite' is true
|
||||
if metadata.get("favorite") and metadata["favorite"].lower() == "true":
|
||||
image_type = "thumbnails"
|
||||
category = os.path.relpath( root, self.input_dir ).replace( os.sep, "/" )
|
||||
filehandler = FileHandler(category=category, image_type=image_type, filename=metadata.get("image"))
|
||||
imageservice = ImageService(self.app)
|
||||
default_size = imageservice.get_image_size( image_type )
|
||||
width = default_size.get( "width" )
|
||||
height = default_size.get( "height" )
|
||||
|
||||
image_tag = imageservice.image_tag(category, image_type, metadata.get("image","Unkown"))
|
||||
print(filehandler.dest_filename_webp)
|
||||
print(image_tag)
|
||||
self.data["favorites"].append({
|
||||
"name": metadata.get("name", "Unknown"),
|
||||
"image": metadata.get("image", "images/default.jpg"),
|
||||
"image": filehandler.dest_filename_webp,
|
||||
"height": height,
|
||||
"width": width,
|
||||
"description": metadata.get("summary", "No description provided"),
|
||||
"path": os.path.relpath(root, self.input_dir).replace(os.sep, "/"),
|
||||
"path": category,
|
||||
|
||||
})
|
||||
|
||||
def generate_json(self):
|
||||
@@ -101,3 +120,4 @@ class MetadataProcessor:
|
||||
json.dump( self.data, json_file, indent = 4, ensure_ascii = False )
|
||||
|
||||
print( f"Generated JSON saved to {self.output_file}" )
|
||||
return True
|
||||
@@ -1,26 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Lidt omkring job situationen og hvordan det fungere</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Bolig Bolig Bolig Bolig - Hvor skal sengen placeres</h1>
|
||||
<p>Nu bliver det spænde!</p>
|
||||
<p>
|
||||
<div class="note">
|
||||
<p>Dette er stadig en test side</p>
|
||||
</div>
|
||||
</p>
|
||||
<p>{img-left-overlay: images/my-cat.png}</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Generalt
|
||||
description: Hvem, hvad og hvor
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 22:16:13 CET 2024
|
||||
summary: Lidt omkring job situationen og hvordan det fungere
|
||||
favorite: true
|
||||
image: images/pic07.jpg
|
||||
---
|
||||
|
||||
# Bolig Bolig Bolig Bolig - Hvor skal sengen placeres
|
||||
|
||||
Nu bliver det spænde!
|
||||
|
||||
{{ note("Dette er stadig en test side") }}
|
||||
|
||||
{img-left-overlay: images/my-cat.png}
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Job
|
||||
description: Hvem, hvad og hvor
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 22:16:13 CET 2024
|
||||
summary: Lidt omkring job situationen og hvordan det fungere
|
||||
favorite: true
|
||||
image: images/pic04.jpg
|
||||
---
|
||||
|
||||
# Lidt mere info om job
|
||||
|
||||
Der skal langt mere tekst her
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{{ note("Husk alpha side") }}
|
||||
@@ -1,57 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Lidt omkring job situationen og hvordan det fungere</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Lidt mere info om job</h1>
|
||||
<p>Der skal langt mere tekst her</p>
|
||||
<p>
|
||||
<div class="note">
|
||||
<p>Husk alpha side</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Lidt omkring job situationen og hvordan det fungere</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Overskrift 1</h1>
|
||||
<h2>Overskrift 2</h2>
|
||||
<h3>Overskrift 3</h3>
|
||||
<h4>Overskrift4</h4>
|
||||
<p>Here is a custom box:</p>
|
||||
<p>
|
||||
<div class="box">
|
||||
<strong>Important Title</strong>
|
||||
<p>This is the content inside the box.</p>
|
||||
</div>
|
||||
</p>
|
||||
<p>Here is a note:</p>
|
||||
<p>
|
||||
<div class="note">
|
||||
<p>This is a note for the readers.</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Job Job Job Job Job
|
||||
description: Hvem, hvad og hvor
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 22:16:13 CET 2024
|
||||
summary: Lidt omkring job situationen og hvordan det fungere
|
||||
favorite: true
|
||||
image: images/pic04.jpg
|
||||
---
|
||||
|
||||
|
||||
# Overskrift 1
|
||||
## Overskrift 2
|
||||
### Overskrift 3
|
||||
#### Overskrift4
|
||||
Here is a custom box:
|
||||
|
||||
{{ box("Important Title", "This is the content inside the box.") }}
|
||||
|
||||
Here is a note:
|
||||
|
||||
{{ note("This is a note for the readers.") }}
|
||||
@@ -1,25 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Lad os snakke kontor fælleskaber</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Kontorfællesskab!</h1>
|
||||
<p>Der skal langt mere tekst her</p>
|
||||
<p>
|
||||
<div class="note">
|
||||
<p>Husk alpha side</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Kontor
|
||||
description: Kontorfælleskaber osv
|
||||
author: Henrik Jess
|
||||
date: today
|
||||
summary: Lad os snakke kontor fælleskaber
|
||||
favorite: false
|
||||
image: images/pic05.jpg
|
||||
---
|
||||
|
||||
# Kontorfællesskab!
|
||||
|
||||
Der skal langt mere tekst her
|
||||
|
||||
|
||||
{{ note("Husk alpha side") }}
|
||||
@@ -1,20 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Skat! - Det skal jo også være sjovt og leve</h1>
|
||||
<p>dette er mere tekst omkring skat</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Skat
|
||||
description: SKAT,SKAT, SKAT - Det skal betales den slags
|
||||
author: Henrik Jess
|
||||
date: today
|
||||
summary: Jeg er langt fra expert, men her er lidt hvad jeg har indsamlet omkring skat
|
||||
favorite: false
|
||||
image: images/pic07.jpg
|
||||
---
|
||||
|
||||
# Skat! - Det skal jo også være sjovt og leve
|
||||
|
||||
dette er mere tekst omkring skat
|
||||
@@ -1,22 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
<p><b>Opsumering: Nørj det er lidt spændende..</b></p>
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><em>Dato: </em></p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Untitled</h2>
|
||||
<div>
|
||||
<h1>Skole start!</h1>
|
||||
<p>dette er mere tekst omkring skole</p>
|
||||
<h1>Skole efter lidt tid</h1>
|
||||
<p>HA!</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Skole
|
||||
description: Skole, ny kultur og menesker
|
||||
author: Erika Nielsen
|
||||
date: today
|
||||
summary: Nørj det er lidt spændende..
|
||||
favorite: false
|
||||
image: images/pic07.jpg
|
||||
---
|
||||
|
||||
# Skole start!
|
||||
|
||||
dette er mere tekst omkring skole
|
||||
|
||||
# Skole efter lidt tid
|
||||
|
||||
HA!
|
||||
@@ -14,6 +14,8 @@ tags: [Portugal, Arbejde, EU-borgere, NIF-nummer, Socialsikring]
|
||||
|
||||
Jeg er ikke selv flyttet til Portugal endnu, men jeg har brugt en del tid på at undersøge, hvad der kræves for at arbejde der som EU-borger. Her er mine noter, baseret på det, jeg har fundet på nettet, YouTube og forskellige guides.
|
||||
|
||||
|
||||
|
||||
Som dansker kan man heldigvis arbejde i Portugal uden at skulle søge om en arbejdstilladelse. Det skyldes EU's regler om fri bevægelighed, så på det punkt er det ret ligetil.
|
||||
|
||||
Men der er nogle praktiske ting, du skal have styr på, før du kan komme i gang:
|
||||
|
||||
77
data/Arbejde/0200_hvordan_gøres_det.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: Arbejde i Portugal
|
||||
description: Mine noter og fund om at arbejde i Portugal som EU-borger
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 22:16:13 CET 2024
|
||||
summary: Men hvordan gør man det så?
|
||||
favorite: false
|
||||
image: images/pic09.jpg
|
||||
category: Job
|
||||
tags: [Portugal, Arbejde, EU-borgere, NIF-nummer, Socialsikring]
|
||||
---
|
||||
## De lidt praktiske ting
|
||||
|
||||
Men der er nogle praktiske ting, du skal have styr på, før du kan komme i gang:
|
||||
|
||||
### **1. Få et NIF-nummer**
|
||||
NIF (Número de Identificação Fiscal) er dit portugisiske skatte-ID, og det er afgørende for at kunne arbejde, åbne en bankkonto eller leje en bolig.
|
||||
|
||||
{{ image('thumbnails', 'carto.jpg', alt='Mit fantatiske billed',css_class="image right") }}
|
||||
|
||||
**Sådan gør du:**
|
||||
1. **Besøg et Finanças-kontor** (det portugisiske skattevæsen) i Portugal.
|
||||
2. Medbring:
|
||||
- Dit pas eller ID-kort.
|
||||
- En midlertidig adresse i Portugal (det kan være en ven, et hotel eller en lejekontrakt).
|
||||
- Hvis du endnu ikke har fast adresse i Portugal, skal du bruge en *fiscal representative* (en person eller et firma i Portugal, der repræsenterer dig skattemæssigt). Der findes mange tjenester, der kan hjælpe online.
|
||||
3. Indsend ansøgningen, og du får normalt dit NIF med det samme.
|
||||
|
||||
|
||||
---
|
||||
|
||||
#### **2. Registrér dig i det portugisiske socialsikringssystem**
|
||||
For at kunne arbejde og få adgang til sundhedsydelser og sociale ydelser, skal du have et socialsikringsnummer (Número de Identificação da Segurança Social).
|
||||
|
||||
**Sådan gør du:**
|
||||
1. Find en lokal *Segurança Social* (socialsikringskontor).
|
||||
2. Medbring:
|
||||
- Dit NIF-nummer.
|
||||
- Din arbejds- eller ansættelseskontrakt (hvis du allerede har en).
|
||||
- Dit pas eller ID-kort.
|
||||
3. Indsend de nødvendige dokumenter. Hvis du er selvstændig, skal du udfylde en særlig formular for freelancere.
|
||||
|
||||
---
|
||||
|
||||
### **3. Åbn en portugisisk bankkonto**
|
||||
En bankkonto er nødvendig for at få løn udbetalt.
|
||||
|
||||
**Sådan gør du:**
|
||||
1. Vælg en bank, og book tid på en filial.
|
||||
2. Medbring:
|
||||
- Dit NIF-nummer.
|
||||
- Dit pas eller ID-kort.
|
||||
- Bevis på adresse (f.eks. en regning eller en lejekontrakt).
|
||||
3. Nogle banker tilbyder også online-åbning, hvilket kan være hurtigere.
|
||||
|
||||
---
|
||||
|
||||
### **4. Meld dig til SEF (Udlændingemyndighederne)**
|
||||
Hvis du planlægger at bo i Portugal i mere end tre måneder, skal du registrere dig hos SEF (Serviço de Estrangeiros e Fronteiras).
|
||||
|
||||
**Sådan gør du:**
|
||||
1. Book tid online via SEF's hjemmeside.
|
||||
2. Medbring:
|
||||
- Dit NIF-nummer.
|
||||
- Bevis på arbejde (ansættelseskontrakt eller anden dokumentation).
|
||||
- Bevis på bopæl i Portugal.
|
||||
3. Når registreringen er færdig, får du dit opholdsbevis.
|
||||
|
||||
---
|
||||
|
||||
### **Gode råd undervejs**
|
||||
- **Sprog:** Mange portugisere taler engelsk, men det kan være en fordel at have en lokal guide eller tolk med, hvis du møder bureaukratiske udfordringer.
|
||||
- **Hjælp:** Overvej at bruge tjenester som advokatfirmaer eller agenturer, der specialiserer sig i at hjælpe udlændinge med at komme i gang i Portugal. Det kan spare tid og frustration.
|
||||
|
||||
Jeg håber, denne trin-for-trin guide kan gøre det nemmere for dig at tage de første skridt. Det er en proces, der kræver lidt planlægning, men med tålmodighed er det helt klart muligt at komme godt i gang!
|
||||
|
||||
{{ box(title="Husk!", content="Jeg har ikke selv gennemgået denne proccess - så jeg har ikke fået den bekræftet endnu, den består rent af andres beretninger og online søgninger") }}
|
||||
BIN
data/Arbejde/images/carto.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
@@ -1,26 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Praktisk info fra min research om arbejde i Portugal</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Kan jeg som udlænding arbejde i Portugal?</h1>
|
||||
<p>Jeg er ikke selv flyttet til Portugal endnu, men jeg har brugt en del tid på at undersøge, hvad der kræves for at arbejde der som EU-borger. Her er mine noter, baseret på det, jeg har fundet på nettet, YouTube og forskellige guides.</p>
|
||||
<p>Som dansker kan man heldigvis arbejde i Portugal uden at skulle søge om en arbejdstilladelse. Det skyldes EU's regler om fri bevægelighed, så på det punkt er det ret ligetil.</p>
|
||||
<p>Men der er nogle praktiske ting, du skal have styr på, før du kan komme i gang:<br />
|
||||
- <strong>NIF-nummer</strong>: Det er et skattemæssigt identifikationsnummer, som du skal bruge til stort set alt i Portugal – arbejde, bankkonto og bolig.<br />
|
||||
- <strong>Socialsikringssystemet</strong>: Du skal registrere dig i det portugisiske socialsikringssystem for at få adgang til sundhed og sociale ydelser.</p>
|
||||
<p>Selvom processen virker enkel på papiret, går det igen i mange kilder, at det kan tage lidt tid, især hvis du ikke taler portugisisk. Flere anbefaler at få hjælp fra nogen, der har prøvet det før, eller bruge lokale rådgivere, hvis det bliver for bøvlet.</p>
|
||||
<p>Jeg deler bare det, jeg har fundet indtil videre, så hvis du også overvejer at arbejde i Portugal, håber jeg, det kan give dig en god start.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -10,18 +10,38 @@ category: Bolig
|
||||
tags: [Portugal, Lissabon, Porto, Algarve, Coimbra, Viseu, Tilflyttere]
|
||||
---
|
||||
|
||||
# Hvilke områder er populære for tilflyttere?
|
||||
# Flytte til Portugal: Populære Områder og Overvejelser
|
||||
|
||||
Når man overvejer at flytte til Portugal, er der nogle områder, som skiller sig ud som særligt attraktive for tilflyttere.
|
||||
|
||||
### Populære områder:
|
||||
- **{{ link_to(title="Lissabon", url="https://www.google.com/maps?q=Lissabon") }}**: Portugals hovedstad med god infrastruktur, et levende kulturliv og mange jobmuligheder.
|
||||
- **{{ link_to(title="Porto", url="https://www.google.com/maps?q=Porto") }}**: Kendt for sin charme, historie og et voksende expat-fællesskab.
|
||||
- **{{ link_to(title="Algarve", url="https://www.google.com/maps?q=Algarve") }}**: Ideelt for dem, der søger sol, strande og en afslappet livsstil.
|
||||
## Populære Områder
|
||||
|
||||
Hvad jeg har googlet mig frem til Spænde byer jeg glæder mig til at besøge i Portugal
|
||||
|
||||
**{{ link_to(title="Lissabon", url="https://www.google.com/maps?q=Lissabon") }}** Som Portugals hovedstad byder Lissabon på en perfekt blanding af historie, moderne faciliteter og et pulserende kulturliv. Byen er et centrum for internationale virksomheder og tilbyder et rigt jobmarked, samtidig med at den imponerer med sine brostensbelagte gader og imponerende udsigter.
|
||||
|
||||
**{{ link_to(title="Porto", url="https://www.google.com/maps?q=Porto") }}** Porto er kendt for sin charme og verdensarvsbeskyttede bymidte. Byen har et blomstrende expat-fællesskab og er perfekt for dem, der søger en mere afslappet atmosfære med nem adgang til vinregionen Douro.
|
||||
|
||||
**{{ link_to(title="Algarve", url="https://www.google.com/maps?q=Algarve") }}** Dette solrige paradis er ideelt for dem, der værdsætter strande, golfbaner og en afslappet livsstil. Algarve er også kendt for at være et yndet sted for pensionister og dem, der ønsker en feriepræget tilværelse året rundt.
|
||||
|
||||
**{{ link_to(title="Braga", url="https://www.google.com/maps?q=Braga") }}** Braga er kendt som Portugals religiøse hovedstad og byder på smukke kirker, historiske monumenter og en ungdommelig vibe takket være et stort antal studerende. Byen er billigere end Lissabon og Porto, men stadig fuld af liv og aktiviteter.
|
||||
|
||||
**{{ link_to(title="Cascais", url="https://www.google.com/maps?q=Cascais") }}** For dem, der søger en luksuriøs livsstil tæt på Lissabon, er Cascais det perfekte valg. Denne kystby kombinerer en afslappet atmosfære med smukke strande, eksklusive boliger og en blomstrende expat-community.
|
||||
|
||||
**{{ link_to(title="Setúbal", url="https://www.google.com/maps?q=Setúbal") }}** Setúbal, syd for Lissabon, tilbyder smukke kyststrækninger og en rig historie. Byen er kendt for sin adgang til Arrábida-bjergene og nogle af landets bedste strande, alt sammen til en overkommelig pris.
|
||||
|
||||
## Alternativer til Storbyerne
|
||||
|
||||
For dem, der ønsker lavere boligpriser og en roligere hverdag, tilbyder Portugal mange alternativer til de travle storbyer:
|
||||
|
||||
**{{ link_to(title="Coimbra", url="https://www.google.com/maps?q=Coimbra") }}** Coimbra er en charmerende universitetsby, der kombinerer en rig historie med lavere leveomkostninger. Byen er ideel for dem, der ønsker at bo i en dynamisk, men mindre hektisk by.
|
||||
|
||||
**{{ link_to(title="Viseu", url="https://www.google.com/maps?q=Viseu") }}** Viseu er kendt for sin høje livskvalitet, smukke landskaber og prisvenlige boligmarked. Byen er perfekt for dem, der søger autentisk portugisisk kultur i en fredelig atmosfære.
|
||||
|
||||
**{{ link_to(title="Évora", url="https://www.google.com/maps?q=Évora") }}** Denne UNESCO-verdensarvsby i Alentejo-regionen er et fantastisk valg for dem, der elsker historie og tradition. Évora er kendt for sine velbevarede romerske ruiner og sin afslappede atmosfære.
|
||||
|
||||
**{{ link_to(title="Aveiro", url="https://www.google.com/maps?q=Aveiro") }}** Også kendt som "Portugals Venedig" byder Aveiro på charmerende kanaler, farverige både og en afslappet kystlivsstil. Byen er ideel for dem, der ønsker en kombination af historie og havudsigt.
|
||||
|
||||
**{{ link_to(title="Guimarães", url="https://www.google.com/maps?q=Guimarães") }}** Guimarães er kendt som "Portugal's fødested" og byder på en rig historie, fantastisk arkitektur og en hyggelig atmosfære. Det er en mindre, men kulturelt rig by, der tilbyder en autentisk portugisisk oplevelse.
|
||||
|
||||
|
||||
### Alternativer til storbyerne:
|
||||
Mindre kendte områder kan være attraktive for dem, der ønsker lavere boligpriser og en roligere livsstil:
|
||||
- **{{ link_to(title="Coimbra", url="https://www.google.com/maps?q=Coimbra") }}**: En universitetsby med historie og lavere leveomkostninger.
|
||||
- **{{ link_to(title="Viseu", url="https://www.google.com/maps?q=Viseu") }}**: Kendt for sin kvalitet af liv, smukke landskab og mere overkommelige boligpriser.
|
||||
|
||||
Uanset hvor i Portugal du vælger at bosætte dig, er der noget for enhver smag – fra travle byer med international atmosfære til mindre byer med ro og autentisk portugisisk kultur.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: Mit kommende område i Portugal
|
||||
description: Planer om at flytte til Porto
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 23:10:00 CET 2024
|
||||
summary: Kort om hvorfor Porto er mit foretrukne område i Portugal
|
||||
favorite: false
|
||||
image: images/pic06.jpg
|
||||
category: Bolig
|
||||
tags: [Portugal, Porto, Flytning, Planlægning]
|
||||
---
|
||||
|
||||
# Hvilket område i Portugal planlægger jeg at flytte til?
|
||||
|
||||
Primært Porto
|
||||
@@ -37,5 +37,4 @@ Hvis du ikke er klar til at købe, er **leje** en god mulighed. Lejepriser varie
|
||||
|
||||
---
|
||||
|
||||
## Konklusion
|
||||
Boligmarkedet i Portugal byder på både muligheder og udfordringer. Mens de populære områder har højere priser, er der stadig gode alternativer i mindre byer. Når man medregner de lavere leveomkostninger i Portugal sammenlignet med Danmark, er der potentiale for at få mere værdi for pengene.
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Populære regioner som Lissabon, Porto og Algarve</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Hvilke områder er populære for tilflyttere?</h1>
|
||||
<p>Når man overvejer at flytte til Portugal, er der nogle områder, som skiller sig ud som særligt attraktive for tilflyttere.</p>
|
||||
<h3>Populære områder:</h3>
|
||||
<ul>
|
||||
<li><strong>
|
||||
<a href="https://www.google.com/maps?q=Lissabon" target="_blank" rel="noopener noreferrer">Lissabon</a>
|
||||
</strong>: Portugals hovedstad med god infrastruktur, et levende kulturliv og mange jobmuligheder.</li>
|
||||
<li><strong>
|
||||
<a href="https://www.google.com/maps?q=Porto" target="_blank" rel="noopener noreferrer">Porto</a>
|
||||
</strong>: Kendt for sin charme, historie og et voksende expat-fællesskab.</li>
|
||||
<li><strong>
|
||||
<a href="https://www.google.com/maps?q=Algarve" target="_blank" rel="noopener noreferrer">Algarve</a>
|
||||
</strong>: Ideelt for dem, der søger sol, strande og en afslappet livsstil.</li>
|
||||
</ul>
|
||||
<h3>Alternativer til storbyerne:</h3>
|
||||
<p>Mindre kendte områder kan være attraktive for dem, der ønsker lavere boligpriser og en roligere livsstil:<br />
|
||||
- <strong>
|
||||
<a href="https://www.google.com/maps?q=Coimbra" target="_blank" rel="noopener noreferrer">Coimbra</a>
|
||||
</strong>: En universitetsby med historie og lavere leveomkostninger.<br />
|
||||
- <strong>
|
||||
<a href="https://www.google.com/maps?q=Viseu" target="_blank" rel="noopener noreferrer">Viseu</a>
|
||||
</strong>: Kendt for sin kvalitet af liv, smukke landskab og mere overkommelige boligpriser.</p>
|
||||
<p>Uanset hvor i Portugal du vælger at bosætte dig, er der noget for enhver smag – fra travle byer med international atmosfære til mindre byer med ro og autentisk portugisisk kultur.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Kort om hvorfor Porto er mit foretrukne område i Portugal</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Hvilket område i Portugal planlægger jeg at flytte til?</h1>
|
||||
<p>Primært Porto</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Brug Idealista og tips til at finde bolig i Portugal</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Hvor finder man bolig?</h1>
|
||||
<p>En af de bedste platforme, jeg har fundet til boligsøgning i Portugal, er <strong>
|
||||
<a href="https://www.idealista.pt/" target="_blank" rel="noopener noreferrer">idealista.pt</a>
|
||||
</strong>. Den minder lidt om DBA og er både overskuelig og nem at bruge. Det er hurtigt at oprette en profil, og tjenesten er tilmed gratis.</p>
|
||||
<h3>Udfordringer ved boligsøgning</h3>
|
||||
<p>Hvis du ikke kender Portugal særlig godt, kan det være svært at beslutte, hvilket område der passer bedst til dine behov. Her er nogle tips:<br />
|
||||
- Brug kortfunktionen på Idealista til at få overblik over priser i forskellige områder.<br />
|
||||
- Overvej at besøge de mest interessante områder først, før du træffer din beslutning.<br />
|
||||
- Tjek lokale Facebook-grupper og fora for tips fra andre tilflyttere.</p>
|
||||
<p>Portugal har meget at byde på, og det rigtige område afhænger af, om du søger storbyliv, strande eller roligere omgivelser.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Boligpriser og ekstra omkostninger i Portugal</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Er det dyrt at købe eller leje bolig i Portugal?</h1>
|
||||
<p>Boligpriser i Portugal varierer meget afhængigt af regionen. I de større byer som <strong>Lissabon</strong> og <strong>Porto</strong> er priserne generelt høje, mens mindre byer som <strong>Coimbra</strong> og <strong>Guarda</strong> tilbyder mere overkommelige muligheder.</p>
|
||||
<hr />
|
||||
<h2>Regionale forskelle</h2>
|
||||
<ul>
|
||||
<li><strong>Lissabon og Porto</strong>: Som de mest populære områder for både tilflyttere og turister har disse byer nogle af de højeste boligpriser i landet.</li>
|
||||
<li><strong>Mindre byer</strong>: Byer som Coimbra, Viseu eller Guarda giver langt billigere alternativer og tilbyder en roligere livsstil.</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>Ekstra omkostninger ved boligkøb</h2>
|
||||
<p>Ud over selve boligprisen skal man være opmærksom på flere ekstra udgifter:<br />
|
||||
- <strong>Ejendomsskat (IMI)</strong>: En årlig skat på mellem <strong>0,3% og 0,45%</strong> af boligens værdi.<br />
|
||||
- <strong>IMT-afgift</strong>: En engangsafgift ved køb af bolig, som afhænger af købsprisen.<br />
|
||||
- <strong>Advokat- og notaromkostninger</strong>: Ved boligkøb er det normalt at bruge advokat og notar for at sikre korrekt papirarbejde.</p>
|
||||
<hr />
|
||||
<h2>Leje vs. køb</h2>
|
||||
<p>Hvis du ikke er klar til at købe, er <strong>leje</strong> en god mulighed. Lejepriser varierer også meget:<br />
|
||||
- I Lissabon og Porto er månedlig husleje typisk højere, men stadig billigere end i de fleste danske storbyer.<br />
|
||||
- I mindre byer kan du finde boliger til markant lavere priser, især hvis du er fleksibel med beliggenheden.</p>
|
||||
<hr />
|
||||
<h2>Konklusion</h2>
|
||||
<p>Boligmarkedet i Portugal byder på både muligheder og udfordringer. Mens de populære områder har højere priser, er der stadig gode alternativer i mindre byer. Når man medregner de lavere leveomkostninger i Portugal sammenlignet med Danmark, er der potentiale for at få mere værdi for pengene. </p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
@@ -5,7 +5,7 @@ author: Henrik Jess
|
||||
date: ons 11 dec 23:25:00 CET 2024
|
||||
summary: Fødevarer er markant billigere i Portugal med få undtagelser som bær og specialvarer.
|
||||
favorite: true
|
||||
image: images/budget2.jpg
|
||||
image: budget2.jpg
|
||||
category: Økonomi
|
||||
tags: [Portugal, Budget, Økonomi]
|
||||
---
|
||||
|
||||
BIN
data/Budget/images/budget2.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
@@ -1,899 +0,0 @@
|
||||
<main>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Fødevarer er markant billigere i Portugal med få undtagelser som bær og specialvarer.</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Jeg har taget udgangspunkt i et indkøb fra Mambeno / Rema1000 indkøb for en uge, og forsøgt og sammenligne det. Her er den fulde kvittering med korrekt <strong>omregning til EUR</strong>, de portugisiske priser og den <strong>procentvise forskel</strong> mellem Danmark og Portugal. Nogen af priserne er konsekvent højere, så er dt fordi der er købt store pakker, eksempelvis Laks. </p>
|
||||
<hr />
|
||||
<p>
|
||||
<div class="box">
|
||||
<strong>Disclaimer</strong>
|
||||
<p>Priserne er baseret på her-og-nu priser fra REMA 1000 i Danmark. Enkelte produkter kan have været på tilbud, hvilket kan påvirke sammenligningen.</p>
|
||||
</div>
|
||||
</p>
|
||||
<h2><strong>Samlet Oversigt</strong></h2>
|
||||
<p><strong>Valutakurs</strong>: 1 EUR = 7,44 DKK</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Nr.</strong></th>
|
||||
<th><strong>Varebeskrivelse</strong></th>
|
||||
<th><strong>Pris i Danmark (EUR)</strong></th>
|
||||
<th><strong>Pris i Portugal (EUR)</strong></th>
|
||||
<th style="text-align: right;"><strong>Forskel (EUR)</strong></th>
|
||||
<th style="text-align: right;"><strong>% Forskel</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>FLUTES</td>
|
||||
<td>0,87</td>
|
||||
<td>0,50</td>
|
||||
<td style="text-align: right;">0,37</td>
|
||||
<td style="text-align: right;">-42,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>TANDPASTA COLGATE</td>
|
||||
<td>3,49</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">1,49</td>
|
||||
<td style="text-align: right;">-42,7%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>SOLSIKKERUGBRØD DET GODE</td>
|
||||
<td>3,43</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">1,43</td>
|
||||
<td style="text-align: right;">-41,7%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>TOILETPAPIR SOFT 3-LAGS</td>
|
||||
<td>2,75</td>
|
||||
<td>1,80</td>
|
||||
<td style="text-align: right;">0,95</td>
|
||||
<td style="text-align: right;">-34,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>JORDBÆR (1 LTR)</td>
|
||||
<td>1,14</td>
|
||||
<td>1,50</td>
|
||||
<td style="text-align: right;">-0,36</td>
|
||||
<td style="text-align: right;">+31,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>SOLBÆR (1 LTR)</td>
|
||||
<td>1,14</td>
|
||||
<td>1,50</td>
|
||||
<td style="text-align: right;">-0,36</td>
|
||||
<td style="text-align: right;">+31,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>HINDBÆR/APPELSIN (1 LTR)</td>
|
||||
<td>1,14</td>
|
||||
<td>1,40</td>
|
||||
<td style="text-align: right;">-0,26</td>
|
||||
<td style="text-align: right;">+22,8%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>MINIMÆLK 0,4%</td>
|
||||
<td>1,61</td>
|
||||
<td>0,90</td>
|
||||
<td style="text-align: right;">0,71</td>
|
||||
<td style="text-align: right;">-44,1%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>RISOTTO M/SVAMPE</td>
|
||||
<td>2,01</td>
|
||||
<td>1,70</td>
|
||||
<td style="text-align: right;">0,31</td>
|
||||
<td style="text-align: right;">-15,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>KÆRGÅRDEN SMØRBAR LET</td>
|
||||
<td>3,49</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">1,49</td>
|
||||
<td style="text-align: right;">-42,7%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11</td>
|
||||
<td>SDJ. SPEGEPØLSE 3-STJERNET</td>
|
||||
<td>2,68</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">0,68</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12</td>
|
||||
<td>OKSE SPEGEPØLSE 3-STJERNET</td>
|
||||
<td>2,68</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">0,68</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>KARTOFFELSPEGEPØLSE 3-STJERNET</td>
|
||||
<td>2,68</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">0,68</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>14</td>
|
||||
<td>KØDPØLSE 3-STJERNET</td>
|
||||
<td>2,41</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">0,41</td>
|
||||
<td style="text-align: right;">-17,1%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>15</td>
|
||||
<td>TORTILLAS HVEDE</td>
|
||||
<td>1,41</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-14,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>16</td>
|
||||
<td>GUACAMOLE DIP</td>
|
||||
<td>1,51</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,31</td>
|
||||
<td style="text-align: right;">-20,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>17</td>
|
||||
<td>SALSASAUCE HOT</td>
|
||||
<td>1,31</td>
|
||||
<td>1,00</td>
|
||||
<td style="text-align: right;">0,31</td>
|
||||
<td style="text-align: right;">-23,7%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>18</td>
|
||||
<td>SMØR ARLA, ØKOLOGISK</td>
|
||||
<td>3,62</td>
|
||||
<td>2,50</td>
|
||||
<td style="text-align: right;">1,12</td>
|
||||
<td style="text-align: right;">-31,0%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>19</td>
|
||||
<td>SKYR NATUREL</td>
|
||||
<td>2,41</td>
|
||||
<td>2,00</td>
|
||||
<td style="text-align: right;">0,41</td>
|
||||
<td style="text-align: right;">-17,1%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>20</td>
|
||||
<td>CHEDDAR OST</td>
|
||||
<td>2,01</td>
|
||||
<td>1,80</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-10,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>21</td>
|
||||
<td>FRAICHE 5%</td>
|
||||
<td>2,68</td>
|
||||
<td>2,20</td>
|
||||
<td style="text-align: right;">0,48</td>
|
||||
<td style="text-align: right;">-17,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>22</td>
|
||||
<td>KRYDDEROST BUKO</td>
|
||||
<td>3,62</td>
|
||||
<td>2,50</td>
|
||||
<td style="text-align: right;">1,12</td>
|
||||
<td style="text-align: right;">-31,0%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>23</td>
|
||||
<td>FRILANDS BRUNCHÆG</td>
|
||||
<td>2,01</td>
|
||||
<td>1,80</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-10,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>24</td>
|
||||
<td>TORTILLA CHIPS SALT</td>
|
||||
<td>1,04</td>
|
||||
<td>1,00</td>
|
||||
<td style="text-align: right;">0,04</td>
|
||||
<td style="text-align: right;">-3,8%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>25</td>
|
||||
<td>GROV CAFESANDWICH</td>
|
||||
<td>2,95</td>
|
||||
<td>2,20</td>
|
||||
<td style="text-align: right;">0,75</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>26</td>
|
||||
<td>FULDKORNSTORTILLAS</td>
|
||||
<td>1,65</td>
|
||||
<td>1,40</td>
|
||||
<td style="text-align: right;">0,25</td>
|
||||
<td style="text-align: right;">-15,2%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>27</td>
|
||||
<td>KYLLINGESCHNITZLER</td>
|
||||
<td>2,68</td>
|
||||
<td>2,30</td>
|
||||
<td style="text-align: right;">0,38</td>
|
||||
<td style="text-align: right;">-14,2%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>28</td>
|
||||
<td>RATATOUILLE</td>
|
||||
<td>1,88</td>
|
||||
<td>1,50</td>
|
||||
<td style="text-align: right;">0,38</td>
|
||||
<td style="text-align: right;">-20,2%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>29</td>
|
||||
<td>FINE ÆRTER</td>
|
||||
<td>1,61</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,41</td>
|
||||
<td style="text-align: right;">-25,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>30</td>
|
||||
<td>FAJITA BLANDING</td>
|
||||
<td>1,88</td>
|
||||
<td>1,40</td>
|
||||
<td style="text-align: right;">0,48</td>
|
||||
<td style="text-align: right;">-25,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>31</td>
|
||||
<td>SOLSIKKEKERNER</td>
|
||||
<td>1,55</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,35</td>
|
||||
<td style="text-align: right;">-22,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>32</td>
|
||||
<td>KOGEPOSE RIS</td>
|
||||
<td>1,01</td>
|
||||
<td>0,90</td>
|
||||
<td style="text-align: right;">0,11</td>
|
||||
<td style="text-align: right;">-10,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>33</td>
|
||||
<td>RASP</td>
|
||||
<td>0,93</td>
|
||||
<td>0,80</td>
|
||||
<td style="text-align: right;">0,13</td>
|
||||
<td style="text-align: right;">-14,0%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>34</td>
|
||||
<td>HASSELNØDDEKERNER</td>
|
||||
<td>1,61</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,41</td>
|
||||
<td style="text-align: right;">-25,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>35</td>
|
||||
<td>PENNE RIGATE</td>
|
||||
<td>1,14</td>
|
||||
<td>1,00</td>
|
||||
<td style="text-align: right;">0,14</td>
|
||||
<td style="text-align: right;">-12,3%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>36</td>
|
||||
<td>ROGN</td>
|
||||
<td>2,14</td>
|
||||
<td>1,80</td>
|
||||
<td style="text-align: right;">0,34</td>
|
||||
<td style="text-align: right;">-15,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>37</td>
|
||||
<td>TOMAT PASTA</td>
|
||||
<td>0,67</td>
|
||||
<td>0,50</td>
|
||||
<td style="text-align: right;">0,17</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>38</td>
|
||||
<td>SØD FRANSK SENNEP</td>
|
||||
<td>1,28</td>
|
||||
<td>1,00</td>
|
||||
<td style="text-align: right;">0,28</td>
|
||||
<td style="text-align: right;">-21,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>39</td>
|
||||
<td>SOJASAUCE</td>
|
||||
<td>1,34</td>
|
||||
<td>1,10</td>
|
||||
<td style="text-align: right;">0,24</td>
|
||||
<td style="text-align: right;">-17,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>40</td>
|
||||
<td>MAJS</td>
|
||||
<td>1,01</td>
|
||||
<td>0,80</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-20,8%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>41</td>
|
||||
<td>HVIDE BØNNER I TOMAT</td>
|
||||
<td>0,93</td>
|
||||
<td>0,80</td>
|
||||
<td style="text-align: right;">0,13</td>
|
||||
<td style="text-align: right;">-14,0%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>42</td>
|
||||
<td>HAKKEDE TOMATER</td>
|
||||
<td>1,01</td>
|
||||
<td>0,80</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-20,8%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>43</td>
|
||||
<td>GRØNTSAGSBOUILLON</td>
|
||||
<td>0,60</td>
|
||||
<td>0,50</td>
|
||||
<td style="text-align: right;">0,10</td>
|
||||
<td style="text-align: right;">-16,7%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>44</td>
|
||||
<td>KRYDDERURTEDRESSING</td>
|
||||
<td>1,68</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,48</td>
|
||||
<td style="text-align: right;">-28,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>45</td>
|
||||
<td>HONNING</td>
|
||||
<td>3,62</td>
|
||||
<td>2,50</td>
|
||||
<td style="text-align: right;">1,12</td>
|
||||
<td style="text-align: right;">-31,0%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>46</td>
|
||||
<td>SØNDERJYSK SPEGEPØLSE</td>
|
||||
<td>1,34</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,14</td>
|
||||
<td style="text-align: right;">-10,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>47</td>
|
||||
<td>LAKSEFILET</td>
|
||||
<td>10,75</td>
|
||||
<td>7,00</td>
|
||||
<td style="text-align: right;">3,75</td>
|
||||
<td style="text-align: right;">-34,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>48</td>
|
||||
<td>KYLLINGEBRYSTFILET</td>
|
||||
<td>8,06</td>
|
||||
<td>5,50</td>
|
||||
<td style="text-align: right;">2,56</td>
|
||||
<td style="text-align: right;">-31,8%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>49</td>
|
||||
<td>TOMATER I BAKKE</td>
|
||||
<td>1,61</td>
|
||||
<td>1,20</td>
|
||||
<td style="text-align: right;">0,41</td>
|
||||
<td style="text-align: right;">-25,5%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>50</td>
|
||||
<td>TØRRET TIMIAN</td>
|
||||
<td>0,67</td>
|
||||
<td>0,50</td>
|
||||
<td style="text-align: right;">0,17</td>
|
||||
<td style="text-align: right;">-25,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>51</td>
|
||||
<td>SPÆD KÅLSALAT</td>
|
||||
<td>3,36</td>
|
||||
<td>2,50</td>
|
||||
<td style="text-align: right;">0,86</td>
|
||||
<td style="text-align: right;">-25,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>52</td>
|
||||
<td>PERSILLE</td>
|
||||
<td>1,21</td>
|
||||
<td>1,00</td>
|
||||
<td style="text-align: right;">0,21</td>
|
||||
<td style="text-align: right;">-17,4%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>53</td>
|
||||
<td>ØKOLOGISKE CITRONER</td>
|
||||
<td>0,81</td>
|
||||
<td>0,70</td>
|
||||
<td style="text-align: right;">0,11</td>
|
||||
<td style="text-align: right;">-13,6%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>54</td>
|
||||
<td>RØD PEBER</td>
|
||||
<td>1,07</td>
|
||||
<td>0,90</td>
|
||||
<td style="text-align: right;">0,17</td>
|
||||
<td style="text-align: right;">-15,9%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>55</td>
|
||||
<td>AGURK</td>
|
||||
<td>0,81</td>
|
||||
<td>0,70</td>
|
||||
<td style="text-align: right;">0,11</td>
|
||||
<td style="text-align: right;">-13,6%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><strong>Opsummering af Prissammenligning mellem Danmark og Portugal</strong></h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Parameter</strong></th>
|
||||
<th><strong>Værdi</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Antal varer sammenlignet</td>
|
||||
<td>55</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gennemsnitlig pris i Danmark (EUR)</td>
|
||||
<td>2,42</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gennemsnitlig pris i Portugal (EUR)</td>
|
||||
<td>1,84</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gennemsnitlig besparelse</td>
|
||||
<td><strong>24,0% lavere i Portugal</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Antal varer billigere i Portugal</td>
|
||||
<td>47</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Antal varer dyrere i Portugal</td>
|
||||
<td>8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>Generel besparelse</strong><br />
|
||||
Priserne i Portugal er <strong>i gennemsnit 24,0% lavere</strong> end i Danmark på tværs af de 55 varer. Dette er særligt tydeligt for basisvarer som smør, mælk og kolonialvarer.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Flest varer er billigere i Portugal</strong><br />
|
||||
Af de 55 varer er <strong>47 billigere</strong> i Portugal, hvilket tydeligt afspejler de lavere leveomkostninger og en mere konkurrencedygtig fødevaresektor.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Eksempler på store besparelser</strong></p>
|
||||
<ul>
|
||||
<li><strong>Kærgården Smørbar Let</strong>: -42,7% billigere</li>
|
||||
<li><strong>Smør Arla Økologisk</strong>: -31,0% billigere</li>
|
||||
<li><strong>Mælk (Minimælk)</strong>: -44,1% billigere</li>
|
||||
<li><strong>Laksefilet</strong>: -34,9% billigere<br />
|
||||
Disse basisvarer oplever en markant prisreduktion i Portugal, hvilket giver væsentlige besparelser i dagligdagen.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Få varer dyrere i Portugal</strong><br />
|
||||
Der er <strong>8 varer</strong>, hvor priserne er højere i Portugal. Eksempler inkluderer:</p>
|
||||
<ul>
|
||||
<li><strong>Jordbær/Solbær</strong>: +31,6% dyrere</li>
|
||||
<li><strong>Hindbær/Appelsin</strong>: +22,8% dyrere<br />
|
||||
Disse prisforskelle kan tilskrives sæsonvariationer og højere importomkostninger. Det kan også begrundes i det kan være danske Tilbuds vare.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Frisk frugt og grønt</strong><br />
|
||||
Generelt er grøntsager som <strong>tomater, agurk og røde peberfrugter</strong> betydeligt billigere i Portugal. Dog er enkelte varer som <strong>bær</strong> dyrere, sandsynligvis på grund af sæsonafhængighed og øgede importomkostninger.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<p>Prissammenligningen viser, at dagligvarer generelt er <strong>betydeligt billigere i Portugal</strong>, især når det gælder smør, mælk og kolonialvarer, som typisk har besparelser på op til <strong>50%</strong>. Dette gør Portugal økonomisk attraktivt for husholdninger. Dog skal man være opmærksom på, at enkelte varer som <strong>bær</strong> og specialvarer kan være dyrere, hvilket er en vigtig faktor i planlægningen af leveomkostninger. </p>
|
||||
<hr />
|
||||
<p>En anden (mere proff) side kommer til næsten de samme konklusioner, man kan måske godt se jeg har brugt Rema1000s tilbuds priser - Men de er relativt tæt på hinanden</p>
|
||||
<ul>
|
||||
<li><strong>Leveomkostninger i Portugal er 38,5% lavere end i Danmark (eksklusive husleje).</strong></li>
|
||||
<li><strong>Leveomkostninger inklusive husleje i Portugal er 32,4% lavere end i Danmark.</strong></li>
|
||||
<li><strong>Huslejepriser i Portugal er 13,1% lavere end i Danmark.</strong></li>
|
||||
<li><strong>Restaurantpriser i Portugal er 53,0% lavere end i Danmark.</strong></li>
|
||||
<li><strong>Dagligvarepriser i Portugal er 33,4% lavere end i Danmark.</strong></li>
|
||||
<li><strong>Den lokale købekraft i Portugal er 54,3% lavere end i Danmark.</strong> </li>
|
||||
</ul>
|
||||
<p>
|
||||
<a href="https://www.numbeo.com/cost-of-living/compare_countries_result.jsp?country1=Denmark&country2=Portugal" target="_blank" rel="noopener noreferrer">Leveomkostningssammenligning mellem Danmark og Portugal - Numbeo</a>
|
||||
- De siger 33.4%<br />
|
||||
|
||||
<a href="https://livingcost.org/cost/denmark/portugal" target="_blank" rel="noopener noreferrer">Leveomkostninger mellem Danmark og Portugal - Livingcost</a>
|
||||
- De siger 28.3% som rammer også meget godt.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Find de bedste steder at handle i Portugal, sammenlignet med danske supermarkeder.</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><strong>Sammenligning af supermarkeder: Danmark vs Portugal</strong></h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Danmark</strong></th>
|
||||
<th><strong>Portugal</strong></th>
|
||||
<th><strong>Bemærkninger</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>REMA 1000</strong></td>
|
||||
<td><strong>Pingo Doce</strong></td>
|
||||
<td>Budgetvenligt supermarked med mange dagligvarer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Føtex</strong> / <strong>Bilka</strong></td>
|
||||
<td><strong>Continente</strong></td>
|
||||
<td>Stort supermarked med bredt udvalg og gode tilbud.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Netto</strong></td>
|
||||
<td><strong>Lidl</strong></td>
|
||||
<td>Lavpris supermarked. Lidl findes i begge lande.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>SuperBrugsen</strong></td>
|
||||
<td><strong>Auchan</strong> (tidl. Jumbo)</td>
|
||||
<td>Mid-range supermarked med fokus på kvalitet.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Irma</strong></td>
|
||||
<td><strong>El Corte Inglés</strong></td>
|
||||
<td>Premium supermarked med internationale varer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Meny</strong></td>
|
||||
<td><strong>Intermarché</strong></td>
|
||||
<td>God kvalitet og et stort udvalg af varer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Aldi</strong></td>
|
||||
<td><strong>Aldi</strong></td>
|
||||
<td>Ensartet kæde i begge lande, kendt for lave priser.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Kvickly</strong></td>
|
||||
<td><strong>Minipreço</strong></td>
|
||||
<td>Prisvenligt med mindre butikker i lokalområder.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><strong>Anbefalede steder at handle i Portugal</strong></h3>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="https://www.pingodoce.pt" target="_blank" rel="noopener noreferrer"><strong>Pingo Doce</strong></a>
|
||||
– Kendt for lave priser og gode tilbud, især på dagligvarer som brød, mælk og grøntsager.</li>
|
||||
<li>
|
||||
<a href="https://www.continente.pt" target="_blank" rel="noopener noreferrer"><strong>Continente</strong></a>
|
||||
– Portugals svar på Bilka/Føtex med alt fra mad til husholdningsartikler. Ofte har de gode rabatprogrammer.</li>
|
||||
<li>
|
||||
<a href="https://www.lidl.pt" target="_blank" rel="noopener noreferrer"><strong>Lidl</strong></a>
|
||||
– Samme kendte koncept som i Danmark. Billige basisvarer og et mindre udvalg.</li>
|
||||
<li>
|
||||
<a href="https://www.auchan.pt" target="_blank" rel="noopener noreferrer"><strong>Auchan</strong></a>
|
||||
(tidligere Jumbo) – Større supermarkeder med et bredt udvalg af varer, inklusiv internationale produkter.</li>
|
||||
<li>
|
||||
<a href="https://www.intermarche.pt" target="_blank" rel="noopener noreferrer"><strong>Intermarché</strong></a>
|
||||
– Fokus på frisk frugt og grønt samt lokale portugisiske produkter.</li>
|
||||
<li>
|
||||
<a href="https://www.minipreco.pt" target="_blank" rel="noopener noreferrer"><strong>Minipreço</strong></a>
|
||||
– Lavprisalternativ med mindre butikker, som er nemme at finde i byområder.</li>
|
||||
<li>
|
||||
<a href="https://www.elcorteingles.pt" target="_blank" rel="noopener noreferrer"><strong>El Corte Inglés</strong></a>
|
||||
– Perfekt til luksusvarer og specialprodukter, men til lidt højere priser.</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<h3><strong>Tips til at spare penge i Portugal</strong></h3>
|
||||
<ul>
|
||||
<li><strong>Frugt og grønt</strong> er ofte billigst på lokale markeder, som fx <strong>"Mercado Municipal"</strong>.</li>
|
||||
<li>Brug <strong>loyalitetskort</strong> i supermarkeder som Pingo Doce og Continente for at få rabatter.</li>
|
||||
<li>Køb kød og fisk på <strong>lokale slagtere</strong> og fiskemarkeder for bedre priser og kvalitet.</li>
|
||||
<li>Planlæg ugentlige indkøb i større butikker som <strong>Continente</strong> for at udnytte kampagner og tilbud. </li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Elregninger er høje, mens vand og gebyrer er moderate</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1>Hvordan påvirker el- og vandregninger leveomkostningerne?</h1>
|
||||
<ul>
|
||||
<li><strong>Elregninger</strong>: Elektricitet i Portugal kan være relativt dyrt sammenlignet med andre europæiske lande. Det er en omkostning, der især mærkes i vintermånederne, hvor opvarmning kan trække prisen op.</li>
|
||||
<li><strong>Vand og affaldsgebyrer</strong>: Disse regninger er typisk moderate og overkommelige, hvilket hjælper med at holde de samlede udgifter nede.</li>
|
||||
</ul>
|
||||
<p>Elektricitetspriserne i Portugal er blandt de højeste i Europa. Ifølge Eurostat var den gennemsnitlige pris for husholdninger i første halvdel af 2024 €0,2426 per kWh, en stigning på 5,52% fra det foregående halvår. Denne stigning skyldes blandt andet landets afhængighed af importerede energikilder, som udgør omkring 65% af det samlede energiforbrug.</p>
|
||||
<p>I modsætning hertil er omkostningerne til vand og affaldshåndtering i Portugal mere moderate. For en lejlighed på 85 m² ligger de månedlige udgifter til el, vand, varme, køling og affaldsbortskaffelse mellem €54 og €150, med et gennemsnit omkring midten af dette interval. Disse omkostninger er relativt lave sammenlignet med andre europæiske lande.</p>
|
||||
<p>For at reducere de høje eludgifter, især i vintermånederne, kan det være fordelagtigt at investere i energieffektive løsninger og apparater. Desuden kan bevidsthed om energiforbrug og valg af tidspunkter med lavere elpriser bidrage til at minimere omkostningerne.</p>
|
||||
<p>Sammenfattende er det vigtigt at tage højde for de relativt høje elpriser i Portugal, mens vand- og affaldsgebyrer forbliver moderate. Ved at implementere energieffektive tiltag kan man opnå en mere balanceret og overkommelig samlet leveomkostning.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="row">
|
||||
<div class="col-8 col-12-medium">
|
||||
|
||||
</div>
|
||||
<div class="col-4 col-12-medium">
|
||||
<p><small> <em>Kort: Leveomkostningerne i Portugal er lave, især på bolig og dagligvarer. Få indsigt i, hvordan du kan leve godt og billigt under sydens sol med et gennemtænkt budget.</em></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Her er det reviderede <strong>husholdningsbudget i Portugal</strong> uden kontorfællesskab og flyrejser, så du kan sammenligne med leveomkostninger i Danmark.</p>
|
||||
<hr />
|
||||
<h2><strong>Månedligt Budget i Portugal</strong></h2>
|
||||
<p>
|
||||
<div class="box">
|
||||
<strong>Disclaimer</strong>
|
||||
<p>Det skal siges jeg har ikke de faktiske tal endnu, så det er 100% på tommelfinger vudering</p>
|
||||
</div>
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Kategori</strong></th>
|
||||
<th><strong>Udgift pr. måned (EUR)</strong></th>
|
||||
<th><strong>Bemærkninger</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Boligleje (100 m²)</strong></td>
|
||||
<td>1.000 - 1.200</td>
|
||||
<td>Afhænger af område (fx Lissabon dyrere)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>El og varme</strong></td>
|
||||
<td>120 - 150</td>
|
||||
<td>Højt i vintermånederne</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Vand og affaldsgebyrer</strong></td>
|
||||
<td>30 - 50</td>
|
||||
<td>Moderate priser</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Internet og mobil</strong></td>
|
||||
<td>40 - 60</td>
|
||||
<td>Kombipakke til hjem og telefoner</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Mad og dagligvarer</strong></td>
|
||||
<td>500 - 600</td>
|
||||
<td>Baseret på ugentlig kvittering (~125 EUR)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Transport (offentlig/benzin)</strong></td>
|
||||
<td>50 - 100</td>
|
||||
<td>Offentlig transport og små ture</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Forsikringer</strong></td>
|
||||
<td>50 - 75</td>
|
||||
<td>Sundhed, indbo, bil osv.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sundhed</strong></td>
|
||||
<td>50 - 100</td>
|
||||
<td>Egenbetaling for tandlæge og medicin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Fritid og aktiviteter</strong></td>
|
||||
<td>100 - 150</td>
|
||||
<td>Restauranter, biograf, hobbyer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Erika's skoleudgifter</strong></td>
|
||||
<td>50 - 100</td>
|
||||
<td>Materialer og småudgifter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Samlet månedlig udgift</strong></td>
|
||||
<td><strong>1.990 - 2.585 EUR</strong></td>
|
||||
<td><strong>Cirka 14.800 - 19.200 DKK</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3><strong>Kort analyse af budgetoversigten</strong></h3>
|
||||
<ol>
|
||||
<li><strong>Boligleje</strong>: Boligleje udgør den største udgiftspost med <strong>1.000 - 1.200 EUR</strong> om måneden, afhængigt af området. Lissabon og større byer er typisk dyrere, mens mindre byer tilbyder mere overkommelige priser.</li>
|
||||
<li><strong>Energiomkostninger (El og varme)</strong>: El og varme koster mellem <strong>120 - 150 EUR</strong>, og dette kan stige i vintermånederne, især hvis opvarmning ikke er energieffektiv. Dette er en vigtig post at planlægge for.</li>
|
||||
<li><strong>Vand og affaldsgebyrer</strong>: Disse udgifter er relativt lave på <strong>30 - 50 EUR</strong>, hvilket hjælper med at holde de samlede faste omkostninger nede.</li>
|
||||
<li><strong>Dagligvarer og mad</strong>: Madbudgettet på <strong>500 - 600 EUR</strong> om måneden er realistisk for to personer og er baseret på en gennemsnitlig ugentlig udgift på <strong>125 EUR</strong>. Dette afspejler rimelige priser på dagligvarer i Portugal.</li>
|
||||
<li><strong>Transport</strong>: Transportomkostninger er beskedne på <strong>50 - 100 EUR</strong>, hvilket inkluderer offentlig transport og mindre biludgifter. Det lave niveau skyldes de lave priser på månedskort og brændstof i Portugal sammenlignet med Nordeuropa.</li>
|
||||
<li><strong>Sundhed og forsikringer</strong>: Sundhedsudgifter og forsikringer varierer mellem <strong>50 - 100 EUR</strong> hver. Dette inkluderer egenbetaling til tandlæge, medicin og basale forsikringer som sundhed og indbo.</li>
|
||||
<li><strong>Fritid og aktiviteter</strong>: Fritidsbudgettet på <strong>100 - 150 EUR</strong> dækker restauranter, biograf og hobbyer. Portugal tilbyder generelt billigere oplevelser, hvilket giver mere økonomisk frihed til fritidsaktiviteter.</li>
|
||||
<li><strong>Skoleudgifter</strong>: Erika's skoleudgifter er moderate med <strong>50 - 100 EUR</strong>, typisk til materialer og mindre udgifter. - Dog med tanke på folkeskole med int. sproglinje (ikke privat)</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<h3><strong>Samlet vurdering</strong></h3>
|
||||
<p>Det samlede budget på <strong>1.990 - 2.585 EUR</strong> om måneden (cirka <strong>14.800 - 19.200 DKK</strong>) viser, at det er muligt at leve komfortabelt i Portugal til en lavere pris end i Danmark.<br />
|
||||
- <strong>Boligleje</strong> er den største omkostning, men stadig overkommelig sammenlignet med danske storbypriser.<br />
|
||||
- <strong>Mad og dagligvarer</strong> udgør en betydelig, men rimelig del af budgettet.<br />
|
||||
- <strong>Faste udgifter</strong> som energi og vand er relativt lave, mens sundhed og fritidsaktiviteter giver økonomisk fleksibilitet.</p>
|
||||
<h2>Dette budget giver plads til en stabil og behagelig hverdag i Portugal uden store kompromiser på livskvalitet.</h2>
|
||||
<h2><strong>Sammenligning med Danmark</strong></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><strong>Kategori</strong></th>
|
||||
<th><strong>Portugal (EUR)</strong></th>
|
||||
<th><strong>Danmark (EUR)</strong></th>
|
||||
<th><strong>Besparelse (%)</strong></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Boligleje (100 m²)</strong></td>
|
||||
<td>1.000 - 1.200</td>
|
||||
<td>1.500 - 2.000</td>
|
||||
<td>-33% til -50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>El og varme</strong></td>
|
||||
<td>120 - 150</td>
|
||||
<td>200 - 250</td>
|
||||
<td>-25% til -40%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Vand og affaldsgebyrer</strong></td>
|
||||
<td>30 - 50</td>
|
||||
<td>60 - 100</td>
|
||||
<td>-50% til -70%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Internet og mobil</strong></td>
|
||||
<td>40 - 60</td>
|
||||
<td>50 - 70</td>
|
||||
<td>-20% til -30%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Mad og dagligvarer</strong></td>
|
||||
<td>500 - 600</td>
|
||||
<td>800 - 1.000</td>
|
||||
<td>-25% til -40%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Transport</strong></td>
|
||||
<td>50 - 100</td>
|
||||
<td>150 - 200</td>
|
||||
<td>-50% til -66%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Forsikringer</strong></td>
|
||||
<td>50 - 75</td>
|
||||
<td>100 - 150</td>
|
||||
<td>-50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sundhed</strong></td>
|
||||
<td>50 - 100</td>
|
||||
<td>100 - 150</td>
|
||||
<td>-33% til -50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Fritid og aktiviteter</strong></td>
|
||||
<td>100 - 150</td>
|
||||
<td>200 - 300</td>
|
||||
<td>-50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Samlet månedlig udgift</strong></td>
|
||||
<td><strong>1.990 - 2.585</strong></td>
|
||||
<td><strong>3.360 - 4.520</strong></td>
|
||||
<td><strong>-30% til -45%</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><strong>Analyse</strong></h3>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>Boligudgifter</strong><br />
|
||||
Bolig er markant billigere i Portugal med en besparelse på <strong>33-50%</strong> sammenlignet med Danmark. Lejepriserne i mindre byer er endnu lavere.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Dagligvarer</strong><br />
|
||||
Indkøb af mad og andre dagligvarer koster i gennemsnit <strong>25-40% mindre</strong> i Portugal, som også reflekteres i din kvittering.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>El og varme</strong><br />
|
||||
Selvom elpriserne er høje i Portugal, er de stadig <strong>25-40% lavere</strong> end i Danmark, hvor opvarmningsomkostninger i vintermånederne er væsentligt højere.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Transport og sundhed</strong><br />
|
||||
Offentlig transport og sundhedsydelser er også væsentligt billigere i Portugal med besparelser på op til <strong>50-66%</strong>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Samlede omkostninger</strong><br />
|
||||
Samlet set er leveomkostningerne i Portugal <strong>30-45% lavere</strong> end i Danmark. Dette giver et stort økonomisk råderum og mulighed for en bedre livskvalitet.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>At bo i Portugal kan give betydelige besparelser på husleje, dagligvarer og generelle leveomkostninger. Selvom elpriserne kan være en udfordring, er det samlede billede væsentligt billigere end i Danmark, især med energieffektive løsninger.</p>
|
||||
</div>
|
||||
<hr>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
44
data/Flytning/0500_lidt_billeder_af_lejligheder.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Lejligheder i Porto
|
||||
description: Detaljer om lejligheder til leje i Porto med praktiske informationer for EU-borgere
|
||||
author: Henrik Jess
|
||||
date: ons 11 dec 23:40:00 CET 2024
|
||||
summary: To lejligheder i Porto med specifikationer, priser og billeder
|
||||
favorite: false
|
||||
image: images/pic04.jpg
|
||||
category: Bolig
|
||||
tags: [Portugal, Bolig, Leje, Porto, EU-borger, Flytning]
|
||||
---
|
||||
|
||||
|
||||
# Lejlighed i Porto
|
||||
|
||||
Dette er en lejlighed beliggende i Porto på Rua 28 de Janeiro, Candal - Regadas, Santa Marinha e São Pedro da Afurada.
|
||||
|
||||
Lejligheden har en månedlig husleje på **1.200 euro** og indeholder følgende:
|
||||
|
||||
- **3 værelser**, hvoraf 1 er et ensuite-værelse
|
||||
- **1 badeværelse**
|
||||
- **1 gæstetoilet**
|
||||
- **Stue og køkken** i åben plan
|
||||
- **1 balkon**, der deles mellem alle fire værelser
|
||||
- **1 balkon** tilknyttet køkkenet
|
||||
- **1 opbevaringsrum** på 10 m²
|
||||
- **2 parkeringspladser**
|
||||
|
||||
{{ slider(options={"width": 500, "height": 500}, images=["lejlighed2/Appartment_20250107_214352.png","lejlighed2/Appartment_20250107_214411.png","lejlighed2/Appartment_20250107_214423.png","lejlighed2/Appartment_20250107_214436.png","lejlighed2/Appartment_20250107_214446.png","lejlighed2/Appartment_20250107_214455.png","lejlighed2/Appartment_20250107_214504.png","lejlighed2/Appartment_20250107_214625.png","lejlighed2/Appartment_20250107_214639.png","lejlighed2/Appartment_20250107_214712.png","lejlighed2/Appartment_20250107_214733.png","lejlighed2/Appartment_20250107_214822.png","lejlighed2/Appartment_20250107_214843.png"]) }}
|
||||
|
||||
---
|
||||
|
||||
Dette er endnu lejlighed beliggende i Porto
|
||||
|
||||
Lejligheden har en månedlig husleje på **1.050 euro** og indeholder følgende:
|
||||
|
||||
- **3 værelser**
|
||||
- **1 badeværelse**
|
||||
- **1 gæstetoilet**
|
||||
- **Stue og køkken** i åben plan
|
||||
- **1 balkon**, der deles mellem alle fire værelser
|
||||
- **1 balkon** tilknyttet køkkenet
|
||||
|
||||
{{ slider(options={"width": 500, "height": 500}, images=["lejlighed1/Appartment_20250106_205457-2.png","lejlighed1/Appartment_20250106_205634.png","lejlighed1/Appartment_20250106_205648.png","lejlighed1/Appartment_20250106_205657.png","lejlighed1/Appartment_20250106_205706.png","lejlighed1/Appartment_20250106_205714.png","lejlighed1/Appartment_20250106_205728-1.png","lejlighed1/Appartment_20250106_205744.png","lejlighed1/Appartment_20250106_205755.png","lejlighed1/Appartment_20250106_205806.png","lejlighed1/Appartment_20250106_205816.png","lejlighed1/Appartment_20250106_205835.png","lejlighed1/Appartment_20250106_205842.png","lejlighed1/Appartment_20250106_205852.png","lejlighed1/Appartment_20250106_205901.png","lejlighed1/Appartment_20250106_205909.png","lejlighed1/Appartment_20250106_205918.png","lejlighed1/Appartment_20250106_205928.png","lejlighed1/Appartment_20250106_205936.png","lejlighed1/Appartment_20250106_205946.png","lejlighed1/Appartment_20250106_205955.png"]) }}
|
||||
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205457-2.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205634.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205648.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205657.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205706.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205714.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205728-1.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205744.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205755.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205806.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205816.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205835.png
Normal file
|
After Width: | Height: | Size: 988 KiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205842.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205852.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205901.png
Normal file
|
After Width: | Height: | Size: 818 KiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205909.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205918.png
Normal file
|
After Width: | Height: | Size: 951 KiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205928.png
Normal file
|
After Width: | Height: | Size: 823 KiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205936.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205946.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
data/Flytning/images/lejlighed1/Appartment_20250106_205955.png
Normal file
|
After Width: | Height: | Size: 721 KiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214352.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214411.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214423.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214436.png
Normal file
|
After Width: | Height: | Size: 911 KiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214446.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214455.png
Normal file
|
After Width: | Height: | Size: 762 KiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214504.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214625.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214639.png
Normal file
|
After Width: | Height: | Size: 816 KiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214712.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
data/Flytning/images/lejlighed2/Appartment_20250107_214733.png
Normal file
|
After Width: | Height: | Size: 864 KiB |