Compare commits

...

56 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
6d0ba453b0 Nomad stuff
All checks were successful
Build and Deploy LifeFAQ / build-image (push) Successful in 51s
2025-10-05 16:13:39 +02:00
Henrik Jess Nielsen
ae902c51f4 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 34s
2025-10-05 16:12:09 +02:00
Henrik Jess Nielsen
4d3f640ecd Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 2m2s
2025-10-05 16:07:20 +02:00
Henrik Jess Nielsen
7f87efbeb7 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 3m10s
2025-10-05 16:01:55 +02:00
Henrik Jess Nielsen
f318440572 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 3m21s
2025-10-05 15:54:42 +02:00
Henrik Jess Nielsen
0747579dcf Nomad stuff
Some checks are pending
Build, Push, and Deploy to Nomad / docker-nomad (push) Waiting to run
2025-10-05 15:53:10 +02:00
Henrik Jess Nielsen
6e97805eea Nomad stuff
Some checks are pending
Build, Push, and Deploy to Nomad / docker-nomad (push) Waiting to run
2025-10-05 15:48:30 +02:00
Henrik Jess
57739f565a [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m10s
2025-02-10 14:28:05 +01:00
Henrik Jess
b5521765a1 [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 2m18s
2025-02-04 18:08:35 +01:00
e4fd13a782 [main] Flight Things
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m54s
2025-01-21 21:04:40 +01:00
ae999e1fac [main] Wtf
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m22s
2025-01-20 01:24:08 +01:00
0176cf85e3 [main] sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m14s
2025-01-20 01:18:07 +01:00
907cf90b11 [main] sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2025-01-15 06:27:14 +01:00
Henrik Jess
e1207362de [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m10s
2025-01-14 19:51:29 +01:00
Henrik Jess
5de92fa7a1 [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 2m8s
2025-01-14 17:25:45 +01:00
0129164fd5 [main] Fucking api fuck lort
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2025-01-13 23:42:16 +01:00
75c7a76210 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 58s
2025-01-07 22:47:50 +01:00
805ff80ce8 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m3s
2025-01-07 22:43:30 +01:00
2b9361c7e9 [main] Content..
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2025-01-07 22:42:46 +01:00
8573e330d6 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-06 21:11:48 +01:00
d1364558ee [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2025-01-05 22:48:09 +01:00
efa5d28d1d Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2025-01-04 03:21:19 +01:00
1f1948e40d Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy with Blue/Green or Canary / build-push (push) Successful in 16s
Build, Push, and Deploy with Blue/Green or Canary / blue-green-deploy (push) Failing after 0s
Build, Push, and Deploy with Blue/Green or Canary / canary-deploy (push) Failing after 0s
2025-01-04 03:17:08 +01:00
0580e5121e Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy with Blue/Green or Canary / build-push (push) Successful in 16s
Build, Push, and Deploy with Blue/Green or Canary / blue-green-deploy (push) Failing after 0s
Build, Push, and Deploy with Blue/Green or Canary / canary-deploy (push) Failing after 0s
2025-01-04 03:13:26 +01:00
f1d1b8ea93 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-04 03:02:40 +01:00
2e878e24f6 Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy to Nomad with Waypoint / docker-waypoint-nomad (push) Failing after 18s
2025-01-04 03:01:28 +01:00
2643f19669 [main] Rollback..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2025-01-04 02:14:26 +01:00
957ded280a [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 23s
2025-01-04 02:11:20 +01:00
d701fc3adf [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 23s
2025-01-04 02:09:54 +01:00
df07f530f6 [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-04 02:08:52 +01:00
09686129b9 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-04 02:07:11 +01:00
1cd4e7d4ee [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 44s
2025-01-04 02:06:14 +01:00
f8b216bea3 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-03 09:52:21 +01:00
9f7e431126 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 44s
2025-01-02 22:57:54 +01:00
08846aa70f [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-02 21:45:32 +01:00
3125f68b66 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2025-01-02 21:44:11 +01:00
269d623ca9 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 48s
2024-12-31 01:15:08 +01:00
ca86177e90 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2024-12-31 00:54:44 +01:00
a126778f16 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-31 00:43:04 +01:00
1b3d03cd70 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-31 00:05:36 +01:00
c329891e2e [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 55s
2024-12-30 23:29:04 +01:00
57dde1df71 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2024-12-30 23:20:54 +01:00
15bda3d3d5 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 48s
2024-12-30 22:43:08 +01:00
5ad5e527a7 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2024-12-30 22:12:12 +01:00
7f08417eb1 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-30 21:37:36 +01:00
13bc417d45 [main] Image webp
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 47s
2024-12-30 20:47:04 +01:00
bf1401c32b [main] Imagesizes 150x150
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2024-12-29 19:34:12 +01:00
2ab66fc0a3 [main] Imagesizes 150x150 2024-12-29 19:26:26 +01:00
0416201742 [main] Imagesizes 150x150
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2024-12-29 18:47:43 +01:00
10de61cb25 [main] Imagesizes in slider
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 53s
2024-12-29 18:12:24 +01:00
e5960232f1 [main] requirements
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2024-12-29 10:59:18 +01:00
6c24ac7ec2 [main] requirements
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2024-12-29 04:34:54 +01:00
0914787be6 [main] Playing with images
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-29 04:34:25 +01:00
0121662530 Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-29 04:34:15 +01:00
95b6c1fa05 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2024-12-24 01:41:46 +01:00
668a1ae7a3 Lets make the frontpage in markdown too 2024-12-24 01:41:29 +01:00
342 changed files with 28833 additions and 822 deletions

View File

@@ -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: Check out code
uses: actions/checkout@v3
- name: Checkout code
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: System info
run: |
uname -a
whoami
- name: Build Docker Image
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} .
- name: Set up Docker Context for Buildx
id: buildx-context
run: |
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: Push Docker Image
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
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
- name: Log in to Docker Registry
run: |
echo "${{ secrets.HARBOR_ROBOT_TOKEN }}" | docker login registry.i80.dk -u "robot\$gitserver" --password-stdin
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
- name: Validate Nomad Job
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job validate .gitea/workflows/nomad-job.hcl
- name: Check for changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
docker:
- 'Dockerfile'
- 'app/**'
- 'requirements.txt'
- name: Stop old deployment
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job stop -purge -no-shutdown-delay lifefaq
continue-on-error: true
- name: Build and push Docker image
if: steps.changes.outputs.docker == 'true'
uses: docker/build-push-action@v5
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
with:
context: .
file: ./Dockerfile
push: true
tags: |
registry.i80.dk/gitea/lifefaq:latest
- name: Test container health
run: |
echo "=== Starting container for health check ==="
- name: Apply Nomad Job
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job run .gitea/workflows/nomad-job.hcl
docker pull 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
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: Update Forwarder Configuration
run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/update_forwarder.py --subdomain lifefaq
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
# - 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 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"

View File

@@ -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

View File

@@ -1,4 +1,4 @@
job "lifefaq" {
job "lifefaq-blue" {
region = "global"
datacenters = ["dc1"]
type = "service"
@@ -6,6 +6,9 @@ job "lifefaq" {
update {
stagger = "60s"
max_parallel = 1
canary = 1
auto_revert = true
auto_promote = true
progress_deadline = "6m"
}
@@ -14,22 +17,18 @@ job "lifefaq" {
network {
port "port-app" {
to = 9210 # Internal application port
to = 9210
}
}
# Register the service with Consul
service {
provider = "consul"
name = "lifefaq"
port = "port-app"
# Traefik-specific tags for routing
tags = [
"blue",
"PORT=${NOMAD_PORT_port-app}"
]
# Define a health check using TCP
check {
name = "tcp_check"
type = "tcp"
@@ -42,7 +41,7 @@ job "lifefaq" {
driver = "docker"
config {
image = "registry.i80.dk/gitea/lifefaq:latest"
image = "registry.i80.dk/gitea/lifefaq:blue"
ports = ["port-app"]
}

View 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
}
}
}
}

View 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
View 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"
}
}

View File

@@ -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

Binary file not shown.

47
Makefile Normal file
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

9
app.py
View File

@@ -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
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,6 +6,9 @@ from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import time
from app.services.metadata_processor import MetadataProcessor
class CategoryController:
def __init__(self,data_file="generated_data.json"):
"""Initialize the controller."""
@@ -28,7 +31,10 @@ class CategoryController:
)
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)

View File

@@ -2,34 +2,38 @@ 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.category_controller import CategoryController
from fastapi.middleware.gzip import GZipMiddleware
from app.services.image_controller import ImageHandler
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.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.")
@@ -38,24 +42,35 @@ 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."""
return self.app

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,72 +0,0 @@
import os
from PIL import Image
class ImageHandler:
def __init__(self, base_dir: str):
"""
Initialize the ImageHandler.
:param base_dir: Base directory for storing and retrieving images.
"""
self.base_dir = base_dir
def get_image_path(self, filename: str) -> str:
"""
Construct the full path for a given image file.
:param filename: Relative filename of the image.
:return: Full path to the image.
"""
return os.path.join(self.base_dir, filename)
def get_resized_image_path(self, filename: str, width: int, height: int) -> str:
"""
Construct the path for a resized image.
:param filename: Original image filename.
:param width: Desired width.
:param height: Desired height.
:return: Path to the resized image.
"""
return os.path.join(self.base_dir, f"resized_{width}x{height}_{filename}")
def resize_and_save(self, original_path: str, resized_path: str, width: int, height: int):
"""
Resize and save the image if it doesn't already exist.
:param original_path: Path to the original image file.
:param resized_path: Path to save the resized image.
:param width: Desired width.
:param height: Desired height.
"""
if not os.path.exists(resized_path):
with Image.open(original_path) as img:
img_resized = img.resize((width, height))
img_resized.save(resized_path, format="JPEG")
def generate_image_tag(self, src: str, width: int, height: int, css_class: str = "", alt: str = "") -> str:
"""
Generate an HTML <img> tag and ensure the image exists with the specified dimensions.
:param src: Relative path to the original image.
:param width: Desired width of the image.
:param height: Desired height of the image.
:param css_class: Optional CSS class to add to the <img> tag.
:param alt: Alternative text for the image.
:return: HTML <img> tag.
"""
original_path = self.get_image_path(src)
if not os.path.isfile(original_path):
raise FileNotFoundError(f"Image not found: {src}")
# Construct resized image path
resized_filename = f"resized_{width}x{height}_{os.path.basename(src)}"
resized_path = self.get_resized_image_path(src, width, height)
# Resize and save the image if necessary
self.resize_and_save(original_path, resized_path, width, height)
# Return the <img> tag
class_attr = f' class="{css_class}"' if css_class else ""
alt_attr = f' alt="{alt}"' if alt else ""
return f'<img src="/{resized_path}" width="{width}" height="{height}"{alt_attr}{class_attr}>'

View 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

View File

@@ -1,8 +1,7 @@
import os
from bs4 import BeautifulSoup
from app.services.markdown_render import render_markdown_with_jinja # Your custom renderer
from fastapi import FastAPI
from app.services.markdown_render import MarkdownRenderer
from jinja2 import Environment, FileSystemLoader
@@ -12,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.
@@ -22,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:
"""
@@ -33,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({

View File

@@ -1,123 +1,172 @@
from pathlib import Path
import sys
import markdown
from fastapi import FastAPI
from jinja2 import Environment, DictLoader
from .image_controller import ImageHandler
# Define Jinja2 custom functions
def img_left_overlay(src):
"""Render an image with overlay."""
return f'''
<div class="img-left-overlay">
<img src="{src}" alt="Overlay Image" loading="lazy">
<div class="overlay-text">Overlay Text</div>
</div>
'''
def box(title, content):
"""Render a box component."""
return f'''
<div class="box">
<strong>{title}</strong>
<p>{content}</p>
</div>
'''
def note(content):
"""Render a note component."""
return f'''
<div class="note">
<p>{content}</p>
</div>
'''
def link_to(title, url):
"""Render a box component."""
return f'''
<a href="{url}" target="_blank" rel="noopener noreferrer">{title}</a>
'''
def warning(content):
"""Render a warning component."""
return f'''
<div class="warning">
⚠️ <p>{content}</p>
</div>
'''
from markupsafe import Markup
from .image_service import ImageService, FileHandler
def slider(options, images):
"""Render a slider using the provided HTML structure."""
import uuid
modal_id = uuid.uuid4().hex.upper()[0:6] # Lets create some uniq modals
width = options.get("width", 500)
height = options.get("height", 375)
html_content = []
html_content.append('<div class="button-stack">')
for i, val in enumerate(images):
modal_id = f"{modal_id}_{i}"
modal_id_next = f"{modal_id}_{i+1}"
if int(len(images))<=int(i+1):
modal_id_next = f"{modal_id}_0"
if i % 2 == 0:
html_content.append(f"""<button onclick="openModal('modal{modal_id}')" class="stacked-button"> <img src="{val}" alt="Lets do better" class="thumbnail" loading="lazy"></button>""".strip())
else:
html_content.append(f"""<button onclick="openModal('modal{modal_id}')" class="stacked-button"> <img src="{val}" alt="Lets do better" class="thumbnail" loading="lazy"></button>""".strip())
html_content.append(f"""<div class="modal" id="modal{modal_id}">
<div class="modal-content">
<h2>Modal {i}</h2>
<img src="{val}" alt="Lets do better" loading="lazy">
<div class="modal-buttons">
<button onclick="closeModal('modal{modal_id}')">Close</button>
<button class="next-btn" onclick="nextModal('modal{modal_id}', 'modal{modal_id_next}')">Next</button>
</div>
</div>
</div>""")
html_content.append( '</div>' )
html = '\n'.join( html_content )
return html
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():
"""Create and configure the Jinja2 environment."""
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
image_handler = ImageHandler(base_dir="static/images")
env.globals.update({
"img_left_overlay": img_left_overlay,
"box": box,
"note": note,
"warning": warning,
"link_to": link_to,
"slider": slider,
"image": image_handler.generate_image_tag, # Add image handler function
def _create_jinja_environment(self) -> Environment:
"""
Create and configure the Jinja2 environment with custom functions.
})
return env
Returns:
Environment: A configured Jinja2 environment.
"""
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
def render_markdown_with_jinja(markdown_content: str):
"""
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
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
Args:
markdown_content (str): Raw Markdown content.
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" loading="lazy">
<div class="overlay-text">Overlay Text</div>
</div>
'''
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 {}
def box(self, title: str, content: str) -> str:
"""Render a box component."""
return f'''
<div class="box">
<strong>{title}</strong>
<p>{content}</p>
</div>
'''
# Step 2: Pass the resulting HTML with Jinja2 custom tags through Jinja2
env = create_jinja_environment()
def note(self, content: str) -> str:
"""Render a note component."""
return f'''
<div class="note">
<p>{content}</p>
</div>
'''
template = env.get_template("base_template")
final_html = template.render(content=intermediate_html)
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>
'''
# Step 3: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
final_output = env.from_string(final_html).render()
def warning(self, content: str) -> str:
"""Render a warning component."""
return f'''
<div class="warning">
⚠️ <p>{content}</p>
</div>
'''
return final_output, metadata
def slider(self, options: dict, images: list) -> str:
"""Render a slider component."""
import uuid
modal_id = uuid.uuid4().hex.upper()[0:6]
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.
Args:
markdown_content (str): Raw Markdown content.
Returns:
tuple: Rendered HTML content and metadata as a dictionary.
"""
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 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 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

View File

@@ -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

View File

@@ -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>

View File

@@ -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}

View File

@@ -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") }}

View File

@@ -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>

View File

@@ -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.") }}

View File

@@ -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>

View File

@@ -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") }}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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!

View File

@@ -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:

View 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") }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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]
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View 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"]) }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

View File

@@ -1,27 +0,0 @@
---
name: Fritidsaktiviteter i Portugal
description: Aktiviteter og oplevelser i Portugals varierede landskab
author: Henrik Jess
date: ons 11 dec 23:50:00 CET 2024
summary: Surfing, vandreture og strande oplev Portugal
favorite: false
image: images/pic07.jpg
category: Fritid
tags: [Portugal, Fritid, Surfing, Vandreture, Strande, Aktiviteter, Vandsport]
---
# Hvilke fritidsaktiviteter kan jeg nyde i Portugal?
Portugal er et fantastisk land for både aktive og afslappende fritidsaktiviteter. Landets varierede landskab betyder, at der er noget for enhver smag.
### Aktive oplevelser
- **Surfing og vandsport**: Portugal er kendt for sine fremragende surfspots, især ved kystområder som **Nazaré**, **Ericeira** og **Peniche**.
- **Vandreture**: De smukke bjergområder som **Serra da Estrela** byder på udfordrende og naturskønne ruter.
- **Cykling**: Mange vælger at udforske landskabet på cykel, hvad enten det er langs kysten eller i det bakkede terræn.
### Afslappende aktiviteter
- **Strandliv**: Portugals kyststrækning har nogle af Europas bedste strande, perfekte til afslapning og solbadning.
- **Vinoplevelser**: Besøg vingårde i **Douro-dalen** og smag på lokalproduceret vin.
- **Kultur og historie**: Tag på opdagelse i charmerende byer som **Sintra**, **Óbidos** og **Évora**.
Uanset om du er til eventyr i naturen eller bare vil slappe af med udsigt over Atlanterhavet, har Portugal masser at byde på.

View File

@@ -1,26 +0,0 @@
---
name: Fordele og ulemper ved at flytte til Portugal
description: De største fordele og ulemper ved at flytte til Portugal
author: Henrik Jess
date: tor 12 dec 01:10:00 CET 2024
summary: Lavere leveomkostninger, mildt klima og bureaukratiske udfordringer
favorite: false
image: images/pic01.jpg
category: Flytning
tags: [Portugal, Fordele, Ulemper, Leveomkostninger, Klima, Bureaukrati, Sprog]
---
# Hvad er fordelene og ulemperne ved at flytte til Portugal?
Jeg har samlet mine tanker og observationer om de største fordele og ulemper ved at flytte til Portugal, baseret på den research jeg har lavet.
### Fordele
- **Lavere leveomkostninger**: Sammenlignet med Danmark er det billigere at bo, spise og leve i Portugal. Det gælder især uden for storbyerne.
- **Mildt klima**: Med mange solskinsdage og milde vintre er vejret i Portugal et stort plus, især hvis man er træt af den danske kulde.
- **Frisk start og livsstil**: Portugal tilbyder en mere afslappet livsstil, som jeg tror, mange kunne have glæde af.
### Ulemper
- **Bureaukrati**: Der er mange historier om, at bureaukratiske processer kan være langsomme og forvirrende, især hvis man ikke taler portugisisk.
- **Sproglige barrierer**: Selvom mange portugisere taler engelsk i de større byer, kan det være en udfordring at klare sig i hverdagen uden at kunne sproget.
Portugal virker som et land med mange muligheder, men det kræver forberedelse og tålmodighed at få det hele til at fungere.

View File

@@ -1,30 +0,0 @@
---
name: Økonomiske konsekvenser ved at flytte til Portugal
description: Fordele og ulemper ved økonomi og skatteforhold i Portugal
author: Henrik Jess
date: tor 12 dec 01:20:00 CET 2024
summary: Lavere omkostninger og ændringer i skatteforhold ved flytning
favorite: false
image: images/pic01.jpg
category: Økonomi
tags: [Portugal, Økonomi, Skat, Leveomkostninger, Boligskatter, Danske ydelser]
---
# Hvilke økonomiske konsekvenser er der for mig ved at flytte til Portugal?
Jeg har gjort mig nogle tanker og undersøgelser om, hvad det økonomisk betyder at flytte til Portugal. Her er, hvad jeg er kommet frem til:
### Fordele
- **Lavere leveomkostninger**: De månedlige udgifter til bolig, mad og transport er typisk lavere end i Danmark, især hvis man vælger at bo uden for de største byer.
- **Skatteforhold**: Afhængigt af ens situation kan skatten være lavere i Portugal, men det kræver en vurdering af de portugisiske regler sammenlignet med de danske.
### Ulemper
- **Danske ydelser**: Hvis man fjerner sin danske adresse og bliver fuldt skattepligtig i Portugal, risikerer man at miste adgang til visse danske ydelser.
- **Boligskatter**: Selvom man kan spare danske boligskatter ved at fjerne sin adresse, skal man være opmærksom på de portugisiske skatter som **IMI** (ejendomsskat) og **IMT** (købsskat).
### Overvejelser
Hvis man planlægger at flytte, er det vigtigt at:
1. Undersøge de nøjagtige skatteforhold og indberetningskrav i Portugal.
2. Tage højde for, hvad det betyder at opgive sin danske bopæl, både økonomisk og administrativt.
Alt i alt ser det ud til, at Portugal kan give nogle økonomiske fordele, men det kræver planlægning og forståelse for, hvordan det påvirker skatten og andre økonomiske forhold.

View File

@@ -1,33 +0,0 @@
---
name: Uddannelsesmuligheder i Portugal for børn
description: Muligheder for gymnasier og internationale skoler i Portugal
author: Henrik Jess
date: tor 12 dec 01:15:00 CET 2024
summary: Internationale skoler og portugisiske gymnasier for børn
favorite: false
image: images/pic02.jpg
category: Uddannelse
tags: [Portugal, Uddannelse, Gymnasier, Internationale skoler, STX, Børn]
---
# Hvad er uddannelsesmulighederne i Portugal for mine børn?
Jeg har brugt tid på at undersøge, hvilke uddannelsesmuligheder der findes i Portugal, særligt fordi min datter på 16 år overvejer at tage en STX (studentereksamen) i Danmark. Her er, hvad jeg har fundet:
### Portugisiske gymnasier
Portugals gymnasiale uddannelser minder på mange måder om det danske system, men både pensum og undervisningssproget er naturligvis anderledes. Hvis man ikke taler portugisisk flydende, kan det være en udfordring, især i begyndelsen.
### Internationale skoler
Et oplagt alternativ er internationale skoler, der tilbyder undervisning på engelsk og ofte følger internationale curriculum som **International Baccalaureate (IB)**.
Nogle fordele ved internationale skoler inkluderer:
- Mulighed for at fortsætte på et globalt anerkendt pensum.
- God støtte til elever, der er nye i landet.
- Mange skoler har en stor blanding af elever fra forskellige nationaliteter.
### Overvejelser
Når man vælger mellem portugisiske gymnasier og internationale skoler, er det værd at tænke over:
- Sprogbarrierer og tilpasning til nyt pensum.
- Økonomien internationale skoler kan være dyrere.
- Hvilken uddannelsesretning der passer bedst til fremtidige planer.
Der er mange gode muligheder, men det kræver lidt research at finde det, der passer bedst til ens børn og familiens behov.

Some files were not shown because too many files have changed in this diff Show More