diff --git a/.env.example b/.env.example
index 898a89e..1dcd64e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,2 @@
IMAGE_TAG=latest
MAIN_PORT=3000
-NG_ALLOWED_HOSTS=fraujulian.xyz,www.fraujulian.xyz,test.fraujulian.xyz
diff --git a/.github/workflows/BuildDev.yml b/.github/workflows/BuildDev.yml
index 5e623fc..9781c01 100644
--- a/.github/workflows/BuildDev.yml
+++ b/.github/workflows/BuildDev.yml
@@ -33,8 +33,10 @@ jobs:
run: |
version=$(node -p 'require("./package.json").version')
docker_tag=$version
+ revision=${GITHUB_RUN_ATTEMPT}
echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
+ echo "revision=$revision" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -54,10 +56,6 @@ jobs:
provenance: false
build-args: |
APP_VERSION=${{ steps.version.outputs.value }}
+ VCS_REF=${{ steps.version.outputs.revision }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
- # ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_tag }}
- labels: |
- org.opencontainers.image.version=${{ steps.version.outputs.value }}
- org.opencontainers.image.revision=${{ github.sha }}
- org.opencontainers.image.source=https://github.com/${{ github.repository }}
diff --git a/.github/workflows/BuildLatest.yml b/.github/workflows/BuildLatest.yml
index 8f8a790..96af3cf 100644
--- a/.github/workflows/BuildLatest.yml
+++ b/.github/workflows/BuildLatest.yml
@@ -32,8 +32,10 @@ jobs:
run: |
version=$(node -p 'require("./package.json").version')
docker_tag=${version//+/-}
+ revision=${GITHUB_RUN_ATTEMPT}
echo "value=$version" >> "$GITHUB_OUTPUT"
echo "docker_tag=$docker_tag" >> "$GITHUB_OUTPUT"
+ echo "revision=$revision" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -53,13 +55,9 @@ jobs:
provenance: false
build-args: |
APP_VERSION=${{ steps.version.outputs.value }}
+ VCS_REF=${{ steps.version.outputs.revision }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.docker_tag }}
- labels: |
- org.opencontainers.image.version=${{ steps.version.outputs.value }}
- org.opencontainers.image.revision=${{ github.sha }}
- org.opencontainers.image.source=https://github.com/${{ github.repository }}
- name: Create GitHub release
env:
diff --git a/.prettierignore b/.prettierignore
index 8d74d1f..781090c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,39 +1,39 @@
-# Paketmanager-Abhaengigkeiten werden nicht formatiert
+# Dependencies
/node_modules
-# Lokale Tool-Caches
-.npm-cache
-.eslintcache
-
-# Generierte Build- und Test-Ausgaben
+# Build, coverage, and generated output
/dist
/coverage
+/.angular
+/public
+src/assets/Resources/CSS/Tailwind.css
+src/assets/optimized
-# Betriebssystem-Dateileichen
-.DS_Store
-Thumbs.db
+# Package manager and tool caches
+.npm-cache
+.eslintcache
+.prettier-cache
+*.tsbuildinfo
-# Editor- und IDE-Konfigurationen
-/.idea
-/.vscode
-
-# Lokale Log- und Debug-Dateien
+# Logs and debug output
+*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
-*.log
-*.tsbuildinfo
-# Lokale Secrets und Umgebungsdateien
+# Local environment and secrets
.env
.env.*
backend/.env
backend/.env.*
-# Angular-CLI-Cache
-/.angular
+# Editor, IDE, and OS files
+/.idea
+/.vscode
+.DS_Store
+Thumbs.db
-# Generierte Tailwind-Ausgabe wird aus dem Quell-CSS erzeugt
-src/assets/Resources/CSS/Tailwind.css
+# Markdown is maintained manually and excluded from Prettier formatting
+*.md
diff --git a/AGENTS.md b/AGENTS.md
index 07d178c..56dd79e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,208 +1,223 @@
-# Repository Instructions
+# Repository Guidelines
-These instructions apply to this repository unless a more specific `AGENTS.md`
-exists in a subdirectory.
+## Project Context
-## Required Verification
+This is an Angular portfolio application using Angular 21, TypeScript, SCSS,
+Karma/Jasmine tests, and static hosting for production output. Production
+application code lives in `src/app`; the client entry point lives in
+`src/main.ts`.
-- Always verify changes with:
- - `npm run check`
- - `npm run test`
-- If verification fails, `npm run fix` may help, but do not assume it resolves
- every issue.
-- Respect the existing `tsconfig`, ESLint, and Prettier configurations.
+Feature views live in folders such as `src/app/home`, `src/app/footer`, and
+`src/app/imprint`. Reusable generic UI building blocks belong in
+`src/app/shared`; reusable services belong in `src/app/services`. Assets and
+language content live in `src/assets` and `src/languages`. Do not manually
+maintain generated or copied output in `public/assets/optimized` or `dist`.
-## Assets
+## Agent Workflow
-- Do not edit generated optimized assets by hand.
-- Update source assets in `src/assets/unoptimized/`.
-- Regenerate optimized assets through the provided scripts, usually via
- `npm run generate:assets` or commands that already include asset generation.
+- Read relevant project files before making changes and follow existing
+ patterns.
+- Always validate every change with `npm run check`.
+- Do not run a build unless the user explicitly asks for it.
+- If `npm run check` fails, read the cause first. `npm run fix` may be used, but
+ it cannot fix every issue.
+- Always follow the active ESLint, Prettier, and `tsconfig` rules.
+- Change only files that are relevant to the task.
+- Do not revert unrelated or unclear local changes.
+- Commit, push, or open PRs only when explicitly asked.
-## Localization
+## Important Commands
-- Keep localized text synchronized between:
- - `src/languages/de.ts`
- - `src/languages/en.ts`
-- When changing user-facing text, update both language files unless the change
- is intentionally language-specific.
+- `npm run dev`: generates assets and starts the Angular development server.
+- `npm run build`: generates assets, builds the production app, and copies
+ static root files. Run this only when explicitly requested.
+- `npm start`: serves the built static app through `scripts/static-server.mjs`.
+- `npm test`: generates assets and starts Angular tests with Karma/Jasmine.
+- `npm run check`: runs linting, format checks, version checks, and source text
+ checks. Always use this as the final validation.
+- `npm run fix`: places version data, runs ESLint fixes, fixes source text, and
+ formats the project.
-## Architecture
+Use Node `>=24.14.0` and npm `>=11.12.0`.
-- Create components with exactly one clear responsibility.
-- Keep components focused on UI, user interaction, and view state.
-- Move business logic into focused services.
-- Structure code by feature or domain where appropriate.
-- Put reusable generic elements in `shared/`.
-- Put domain-specific logic and components in the relevant feature folder.
-- Separate container and presentational components when a view becomes complex.
-- Avoid cyclic dependencies between modules, services, and components.
-- Add abstractions only when at least two real use cases exist.
-- Keep services focused on one business or technical responsibility.
+## Code Style
-## TypeScript
-
-- Keep strict TypeScript enabled.
-- Use explicit types for public methods, service APIs, and complex return
- values.
-- Prefer `unknown` over `any` when a value still needs validation.
-- Use `any` only when technically necessary and document the reason.
-- Use union types for fixed value ranges.
-- Use `readonly` for data that should not be mutated.
-- Prefer `const` over `let` when reassignment is not needed.
-- Check `null` and `undefined` explicitly before accessing nested values.
-- Separate API DTOs from internal UI or domain models.
-- Map external API data deliberately into internal models.
-- Use clear names for interfaces, types, classes, variables, inputs, and
- outputs.
-- Avoid type casts when clean type narrowing is possible.
+- TypeScript strict mode is enabled. `strictTemplates` is enabled.
+- Prettier is authoritative: 2 spaces, single quotes, semicolons, trailing
+ commas, and default print width 100.
+- Markdown wraps at 80 characters.
+- Angular selectors use the `app` prefix.
+- Components follow the pattern `feature.component.ts`,
+ `feature.component.html`, and `feature.component.spec.ts`.
+- Services follow the pattern `name.service.ts`.
+- Place shared types in `*.types.ts` files when that matches the local pattern.
+- Explicitly type public methods, service APIs, and complex return values.
+- Use `unknown` instead of `any` when a value must be checked first.
+- Use `any` only when technically necessary, and document the reason.
+- Use `const` instead of `let` when reassignment is not needed.
+- Use `readonly` for data that should not be changed.
+- Do not use non-null assertions to hide real nullability issues.
- Do not suppress type errors with `as unknown as`.
-- Do not use non-null assertions to hide real nullability problems.
+- Use union types for fixed value sets instead of magic strings.
+- Keep API DTOs separate from internal UI or domain models.
+
+## Angular Architecture
+
+- Components must have exactly one clear responsibility.
+- Keep components primarily responsible for UI, user interaction, and view state.
+- Put business logic, HTTP access, mapping, and shared state into focused
+ services.
+- Structure code by features or domains, not only by file types.
+- Put domain-specific logic and domain-specific components in the relevant
+ feature folder.
+- Put generic reusable elements in `shared/`.
+- Split container components from presentational components when a view becomes
+ complex.
+- Create abstractions only when at least two real use cases exist.
+- Avoid cyclic dependencies between components, services, and modules.
+- Do not use services as unordered global storage.
## Angular Components
-- Use `OnPush` change detection unless there is a clear reason not to.
-- Use inputs for externally provided data.
-- Use outputs for events emitted to parent components.
-- Use required inputs when a component cannot work without the data.
+- Use `ChangeDetectionStrategy.OnPush` unless there is a concrete reason not to.
+- Use required inputs when a component cannot work meaningfully without the
+ data.
+- Name inputs and outputs clearly after their purpose.
+- Do not mutate input data directly.
- Keep component classes small and readable.
-- Extract repeated UI blocks into dedicated components.
-- Use lifecycle hooks only when they are necessary.
-- Keep constructors limited to dependency injection.
-- Initialize data clearly and predictably.
-- Do not make god components.
-- Do not mix API access, mapping, business logic, and UI logic in one component.
-- Prefer composition over component inheritance.
+- Extract repeated UI blocks into their own components.
+- Use lifecycle hooks only when they are truly necessary.
+- Keep constructors free of logic; prefer dependency injection with `inject()`
+ when it fits the existing code.
+- Do not manipulate the DOM directly when Angular APIs are sufficient.
+- Do not use component inheritance when composition is simpler.
-## Angular State
+## State, RxJS, and Data Flows
- Use Signals for local synchronous component state.
+- Use computed Signals for derived synchronous values.
- Use RxJS for asynchronous data streams and complex event chains.
-- Use services for shared state across multiple components.
+- Encapsulate shared state in services.
- Keep state as close as possible to where it is needed.
-- Avoid global state when local state is enough.
-- Make state changes explicit and understandable.
-- Avoid hidden state mutation across multiple services.
-- Do not store derived values redundantly when they can be computed.
-
-## RxJS
-
+- Make state changes explicit and easy to follow.
- Name Observables with a `$` suffix.
-- Use the `async` pipe when a stream is displayed directly in a template.
-- Use `switchMap` for requests where older requests should be canceled.
-- Use `concatMap` when ordering must be guaranteed.
-- Use `mergeMap` when parallel processing is intended.
-- Use `exhaustMap` when new events should be ignored while work is running.
-- Use `combineLatest` for dependent live values.
-- Use `forkJoin` for multiple one-time requests that complete together.
-- Clean up manual subscriptions with `takeUntilDestroyed` or an equivalent.
-- Handle errors with `catchError` at the appropriate level.
-- Return controlled fallback state when useful.
-- Do not hide errors that matter for debugging or UX.
-- Do not use `subscribe()` inside `subscribe()`.
+- Use the `async` pipe when a stream is displayed directly in the template.
+- Choose `switchMap`, `concatMap`, `mergeMap`, and `exhaustMap` deliberately
+ based on their semantics.
+- Use `combineLatest` for dependent live values and `forkJoin` for multiple
+ one-time requests.
+- End manual subscriptions with `takeUntilDestroyed` or an equivalent mechanism.
+- Do not write `subscribe()` inside `subscribe()`.
+- Handle errors with `catchError` in the right place and do not blindly swallow
+ them with `EMPTY`.
- Do not use RxJS for trivial synchronous state.
-- Avoid duplicate identical requests from independent subscriptions.
## HTTP and API
- Encapsulate all HTTP calls in services.
- Define response types for API responses.
-- Use central API configuration through environments or injection tokens.
-- Use interceptors for repeated technical concerns such as auth, headers, and
- error handling.
-- Implement loading, error, and empty states where requests affect UI.
-- Treat API data defensively when quality is uncertain.
-- Validate or normalize external data before broad UI usage.
-- Avoid duplicate API calls through shared streams or caching where appropriate.
+- Keep API configuration centralized through environments or injection tokens.
+- Use interceptors for recurring technical tasks such as auth, headers, or error
+ handling.
+- Include loading, error, and empty states for slow or failed requests.
+- Treat external data defensively, and validate or normalize it before broad UI
+ use.
+- Map external API data deliberately into internal models.
+- Avoid duplicate API calls through shared stream or cache handling.
- Do not hardcode API URLs, tokens, roles, or secrets in components.
## Templates
- Keep templates declarative and easy to read.
-- Use templates for presentation and simple bindings only.
-- Move complex conditions into component code or computed signals.
+- Use templates only for presentation and simple bindings.
+- Move complex conditions into component code or computed Signals.
- Use `@if` for conditional rendering.
-- Use `@for` with `track` for lists.
-- Track with a stable unique ID when available.
+- Always use `@for` with `track`.
+- Use stable unique IDs for `track`; use the index only when no stable ID
+ exists.
+- Do not put business rules, complex ternaries, or expensive method calls in
+ templates.
+- Use pipes only for pure formatting.
+- Break large templates into smaller components.
- Keep binding expressions short.
-- Do not put business rules or complex ternaries into templates.
-- Do not call expensive methods directly from templates.
-- Split large templates into smaller components.
## Forms
- Use Reactive Forms for complex forms.
- Define validators explicitly.
- Show validation errors clearly in the UI.
+- Validate critical rules again on the backend.
- Keep form models and API DTOs separate when they differ.
-- Map form data deliberately before submitting.
-- Disable submit while requests are running when duplicate submits are a risk.
-- Surface server-side validation errors in the form.
+- Map form data deliberately before sending it.
+- Disable submit during active requests when repeated submission is problematic.
+- Show server errors visibly in the form.
## Styling
- Prefer component-scoped styling.
-- Use global styles only for truly global rules.
-- Use a consistent design system or consistent utility classes.
-- Reuse classes or components for repeated UI patterns.
-- Account for responsive design in every new view.
-- Prefer semantic HTML before complex CSS.
+- Use global styles in `src/styles.scss` only for truly global rules.
+- Use consistent design tokens, utility classes, or reusable components for
+ recurring UI patterns.
+- Consider responsive design for every new view.
+- Use semantic HTML before adding complex CSS.
- Keep spacing, colors, and font sizes consistent.
-- Use inline styles only for dynamic exceptions.
+- Use inline styles only for dynamic edge cases.
- Do not use `::ng-deep` as a default solution.
-- Do not create global CSS rules that unintentionally affect other components.
-- Avoid fixed pixel layouts without responsive behavior.
+- Do not write global CSS rules that unintentionally affect unrelated
+ components.
+- Do not build fixed-pixel layouts without responsive behavior.
## Security
- Treat all external data as potentially unsafe.
- Never trust user input without validation.
-- Do not store secrets in frontend code.
-- Implement roles and permissions server-side.
-- Use frontend role checks only for UX, not as a security boundary.
-- Render API-provided HTML only after controlled sanitization.
-- Use `bypassSecurityTrustHtml` only after explicit security review.
-- Validate redirect URLs against an allowlist.
+- Do not store secrets in the frontend or repository.
+- Use `.env.example` as the documented template; keep real environment-specific
+ values out of version control.
+- Enforce roles and permissions server-side; use frontend checks only for UX.
+- Render HTML from APIs only after controlled sanitization.
+- Use `bypassSecurityTrustHtml` only after an explicit security review.
+- Check redirect URLs against an allowlist.
- Protect auth flows against XSS and CSRF risks.
- Store tokens only with a deliberate security concept.
## Performance
-- Use lazy loading for larger features.
+- Lazy-load larger features.
- Use `OnPush` to reduce unnecessary change detection.
-- Use `track` in lists.
-- Use virtual scrolling for large lists.
-- Avoid expensive computations in templates.
-- Move expensive computations into computed signals, memoization, or services.
-- Avoid large dependencies for small helper functions.
-- Check bundle size impact before adding libraries.
+- Always render lists with stable `track` values.
+- Consider virtual scrolling for large lists.
+- Do not run expensive calculations in templates.
+- Move expensive calculations into computed Signals, memoization, or services.
+- Do not add a large dependency for a small helper function.
+- Check value, risk, and bundle cost before adding a dependency.
- Optimize images and assets.
-- Measure performance issues before adding complex optimizations.
+- Measure performance problems before implementing complex optimizations.
-## Testing
+## Tests
-- Test services that contain business logic.
+- Keep tests next to the code under test with `*.spec.ts`.
+- Test services that contain domain logic.
- Test complex component logic.
-- Test critical user flows with E2E tests where appropriate.
+- Cover critical user flows with E2E tests when an E2E setup exists or is
+ requested.
- Mock external dependencies in unit tests.
-- Avoid real HTTP calls in unit tests.
-- Write tests with clear behavioral assertions.
-- Cover edge cases and error paths.
-- Keep tests independent from internal implementation details.
-- Use test names that describe behavior.
-- Do not write tests only for coverage.
-- Do not use snapshots as a substitute for concrete assertions.
+- Do not use real HTTP calls in unit tests.
+- Write tests with a clear domain-level assertion.
+- Test edge cases and failure cases.
+- Do not couple tests unnecessarily to implementation details.
+- Test names should describe behavior rather than implementation.
-## Code Quality
+## Code Quality and Project Practice
-- Use ESLint and Prettier.
-- Keep naming consistent.
- Remove unused code and unused imports.
-- Do not leave commented-out code blocks.
-- Keep changes focused.
-- Write commits with concrete messages.
-- Use comments only for context that is not obvious from the code.
+- Do not leave commented-out code blocks in the repository.
+- Leave TODOs only with context, a ticket, or a documented decision.
+- Use comments sparingly and only for context that is not obvious from the code.
- Keep files small enough to understand quickly.
- Follow existing project conventions.
-- Do not leave TODOs without context, a ticket, or a documented decision.
-- Do not add dependencies without checking value, risk, and bundle cost.
+- Prefer small, focused pull requests.
+- Write commit messages with a concrete statement, for example `fix:` or
+ `improve:` followed by a short summary.
+- Do not write commits named only `fix`, `stuff`, `update`, or `changes`.
+- Document workarounds when they are unavoidable.
diff --git a/Dockerfile b/Dockerfile
index 928799a..ab698af 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,6 @@
+ARG VCS_REF=unknown
+ARG APP_VERSION=unknown
+
FROM node:24.14.0-bookworm-slim AS build
WORKDIR /app
@@ -9,24 +12,26 @@ RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build
-FROM node:24.14.0-bookworm-slim AS prod-deps
-
-WORKDIR /app
-ENV HUSKY=0
-
-COPY package.json package-lock.json ./
-RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
-
FROM node:24.14.0-bookworm-slim AS runtime
+ARG VCS_REF
+ARG APP_VERSION
+
WORKDIR /app
ENV NODE_ENV=production
-COPY --from=prod-deps --chown=node:node /app/node_modules ./node_modules
+LABEL org.opencontainers.image.title="Personal Portfolio Website"
+LABEL org.opencontainers.image.description="👋 My portfolio website built with Angular v21 — a concise introduction to who I am and what I build."
+LABEL org.opencontainers.image.licenses="GPL-3.0-only"
+LABEL org.opencontainers.image.version="${APP_VERSION}"
+LABEL org.opencontainers.image.revision="${VCS_REF}"
+LABEL org.opencontainers.image.source="https://github.com/FrauJulian/Personal-Portfolio-Website"
+
COPY --from=build --chown=node:node /app/dist ./dist
+COPY --chown=node:node scripts/static-server.mjs ./scripts/static-server.mjs
USER node
EXPOSE 3000
-CMD ["node", "dist/server/server.mjs"]
+CMD ["node", "scripts/static-server.mjs"]
diff --git a/README.md b/README.md
index f4a6bca..c1e0ae1 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,16 @@
-# 🖼️ FrauJulian's Portfolio Website
+# FrauJulian's Portfolio Website
-### [🙋♂️ Website Preview](https://fraujulian.xyz/)
+### [Website Preview](https://fraujulian.xyz/)
-
+
-## 👂 Languages / Framework / Packages
+## Languages / Framework / Packages
-- Angular v21 (with SSR)
+- Angular v21
- TypeScript
- SCSS
-- Angular Material
-- FontAwesome
-## 💻 Development
+## Development
### Development Preview
@@ -26,31 +24,32 @@ npm run dev
npm run build
```
-This will compile your project and store the build artifacts in the `dist/`
-directory. The production build optimizes your application for performance and
-speed.
+This compiles the project and stores the build artifacts in the `dist/`
+directory. The production build is intended for static hosting.
-### Start compiled Project
+### Start Compiled Project
```bash
npm run start
```
-## 🔍 Code Quality
+This serves the generated static build through `scripts/static-server.mjs`.
-### Lint & Format check
+## Code Quality
+
+### Lint and Format Check
```bash
npm run check
```
-### Auto-fix lint & format issues
+### Auto-fix Lint and Format Issues
```bash
npm run fix
```
-## 🧪 Running Unit Tests
+## Running Unit Tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test
runner, use the following command:
@@ -59,10 +58,11 @@ runner, use the following command:
npm run test
```
-## 🤝 Enjoy?
+## Enjoy?
-~ made by [**FrauJulian**](https://fraujulian.xyz/).
-Give it a star ⭐ on
-[GitHub](https://github.com/FrauJulian/Personal-Portfolio-Website)!
+Made by [FrauJulian](https://fraujulian.xyz/).
-### Greetings from Austria! ⛰️
+Give it a star on
+[GitHub](https://github.com/FrauJulian/Personal-Portfolio-Website).
+
+### Greetings from Austria!
diff --git a/angular.json b/angular.json
index 792344a..c4b4ee9 100644
--- a/angular.json
+++ b/angular.json
@@ -35,12 +35,7 @@
}
],
"styles": ["src/styles.scss"],
- "scripts": [],
- "server": "src/main.server.ts",
- "prerender": true,
- "ssr": {
- "entry": "src/server.ts"
- }
+ "scripts": []
},
"configurations": {
"production": {
@@ -84,7 +79,7 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
- "port": 3000
+ "port": 4200
},
"configurations": {
"production": {
diff --git a/docker-compose.yml b/docker-compose.yml
index bafb2fc..603bf2d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,14 +1,9 @@
services:
portfolio:
- image: docker.lechner-systems.at/lechnersystems/portfolio:${IMAGE_TAG}
- build:
- context: .
- dockerfile: Dockerfile
+ image: docker.lechner-systems.at/lechnersystems/portfolio:${IMAGE_TAG:-latest}
restart: unless-stopped
- env_file:
- - .env
dns:
- 192.168.100.2
- 192.168.100.6
ports:
- - ${MAIN_PORT}:3000
+ - '${MAIN_PORT:-3000}:3000'
diff --git a/npm-zoey.conf b/npm.md
similarity index 79%
rename from npm-zoey.conf
rename to npm.md
index 31baeb8..0f83545 100644
--- a/npm-zoey.conf
+++ b/npm.md
@@ -1,12 +1,16 @@
-Create a new custom location:
+# Nginx Proxy Manager Notes
-Location: ~*
-Path: \.(?:js|css|mjs|webp|avif|png|jpg|jpeg|gif|svg|ico|woff2?)$
-Scheme/Hostname/Port: same as Origin
-Extra Options:
+## Custom Location
----
+Create a new custom location with these values:
+- Location: `~*`
+- Path: `\.(?:js|css|mjs|webp|avif|png|jpg|jpeg|gif|svg|ico|woff2?)$`
+- Scheme/Hostname/Port: `same as Origin`
+
+Use the following extra options:
+
+```nginx
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
@@ -17,13 +21,13 @@ proxy_buffering on;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Vary "Accept-Encoding" always;
+```
----
+## General Configuration
-General Configuration:
-
----
+Use the following general proxy configuration:
+```nginx
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
@@ -57,7 +61,6 @@ gzip_types
text/plain
text/xml;
-# Nur aktiv lassen, wenn dein NPMplus-Image brotli unterstützt.
brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
@@ -75,3 +78,4 @@ brotli_types
add_header Cache-Control "no-cache, must-revalidate" always;
add_header Vary "Accept-Encoding" always;
+```
diff --git a/package.json b/package.json
index 232a80b..f842080 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"------ STARTUP": "",
"test": "npm run generate:assets && ng test",
"dev": "npm run generate:assets && ng serve",
- "start": "node dist/server/server.mjs",
+ "start": "node scripts/static-server.mjs",
"------ CONFIGS": "",
"prepare": "husky"
},
diff --git a/scripts/static-server.mjs b/scripts/static-server.mjs
new file mode 100644
index 0000000..7866344
--- /dev/null
+++ b/scripts/static-server.mjs
@@ -0,0 +1,155 @@
+import { createReadStream } from 'node:fs';
+import { access, readdir, stat } from 'node:fs/promises';
+import { createServer } from 'node:http';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const appRoot = path.resolve(__dirname, '..');
+const distRoot = path.join(appRoot, 'dist');
+const port = Number.parseInt(process.env.PORT ?? '3000', 10);
+
+const mimeTypes = new Map([
+ ['.css', 'text/css; charset=utf-8'],
+ ['.gif', 'image/gif'],
+ ['.html', 'text/html; charset=utf-8'],
+ ['.ico', 'image/x-icon'],
+ ['.jpg', 'image/jpeg'],
+ ['.jpeg', 'image/jpeg'],
+ ['.js', 'text/javascript; charset=utf-8'],
+ ['.json', 'application/json; charset=utf-8'],
+ ['.map', 'application/json; charset=utf-8'],
+ ['.png', 'image/png'],
+ ['.svg', 'image/svg+xml'],
+ ['.txt', 'text/plain; charset=utf-8'],
+ ['.webp', 'image/webp'],
+ ['.woff', 'font/woff'],
+ ['.woff2', 'font/woff2'],
+ ['.xml', 'application/xml; charset=utf-8'],
+]);
+
+function getContentType(filePath) {
+ return mimeTypes.get(path.extname(filePath).toLowerCase()) ?? 'application/octet-stream';
+}
+
+function getCacheControl(filePath) {
+ const normalizedPath = filePath.replace(/\\/g, '/');
+ const fileName = path.basename(filePath);
+ const extension = path.extname(fileName).toLowerCase();
+
+ if (extension === '.html') {
+ return 'no-cache, must-revalidate';
+ }
+
+ if (/\-[A-Z0-9]{8,}\.(css|js)$/i.test(fileName)) {
+ return 'public, max-age=31536000, immutable';
+ }
+
+ if (
+ [
+ '.css',
+ '.gif',
+ '.ico',
+ '.jpg',
+ '.jpeg',
+ '.js',
+ '.json',
+ '.map',
+ '.png',
+ '.svg',
+ '.txt',
+ '.webp',
+ '.woff',
+ '.woff2',
+ '.xml',
+ ].includes(extension)
+ ) {
+ return normalizedPath.includes('/browser/')
+ ? 'public, max-age=604800, stale-while-revalidate=86400'
+ : 'public, max-age=86400, stale-while-revalidate=3600';
+ }
+
+ return 'public, max-age=3600';
+}
+
+async function pathExists(targetPath) {
+ try {
+ await access(targetPath);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function resolveBrowserRoot() {
+ const directBrowserRoot = path.join(distRoot, 'browser');
+ if (await pathExists(path.join(directBrowserRoot, 'index.html'))) {
+ return directBrowserRoot;
+ }
+
+ const distEntries = await readdir(distRoot, { withFileTypes: true });
+ for (const entry of distEntries) {
+ if (!entry.isDirectory()) {
+ continue;
+ }
+
+ const nestedBrowserRoot = path.join(distRoot, entry.name, 'browser');
+ if (await pathExists(path.join(nestedBrowserRoot, 'index.html'))) {
+ return nestedBrowserRoot;
+ }
+ }
+
+ throw new Error(`Could not locate a browser build under ${distRoot}`);
+}
+
+const browserRoot = await resolveBrowserRoot();
+
+async function resolveFile(urlPath) {
+ const normalized = path.posix.normalize(decodeURIComponent(urlPath));
+ const relativePath = normalized.replace(/^(\.\.(\/|\\|$))+/, '').replace(/^\/+/, '');
+
+ const distCandidate = path.join(distRoot, relativePath);
+ if ((await pathExists(distCandidate)) && (await stat(distCandidate)).isFile()) {
+ return distCandidate;
+ }
+
+ const browserCandidate = path.join(browserRoot, relativePath);
+ if ((await pathExists(browserCandidate)) && (await stat(browserCandidate)).isFile()) {
+ return browserCandidate;
+ }
+
+ if (!path.posix.extname(normalized)) {
+ return path.join(browserRoot, 'index.html');
+ }
+
+ return null;
+}
+
+const server = createServer(async (request, response) => {
+ try {
+ const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
+ const filePath = await resolveFile(requestUrl.pathname);
+
+ if (!filePath) {
+ response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+ response.end('Not found');
+ return;
+ }
+
+ response.writeHead(200, {
+ 'Cache-Control': getCacheControl(filePath),
+ 'Content-Type': getContentType(filePath),
+ });
+ createReadStream(filePath).pipe(response);
+ } catch (error) {
+ response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
+ response.end('Internal server error');
+ console.error(error);
+ }
+});
+
+server.listen(port, '0.0.0.0', () => {
+ console.log(`Serving static app from ${browserRoot} on port ${port}`);
+});
diff --git a/src/app/app.config.server.ts b/src/app/app.config.server.ts
deleted file mode 100644
index 11ade97..0000000
--- a/src/app/app.config.server.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { provideServerRendering } from '@angular/ssr';
-import type { ApplicationConfig } from '@angular/core';
-import { mergeApplicationConfig } from '@angular/core';
-
-import { appConfig } from './app.config';
-
-const serverConfig: ApplicationConfig = {
- providers: [provideServerRendering()],
-};
-
-export const config: ApplicationConfig = mergeApplicationConfig(appConfig, serverConfig);
diff --git a/src/main.server.ts b/src/main.server.ts
deleted file mode 100644
index b7c62b3..0000000
--- a/src/main.server.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { BootstrapContext } from '@angular/platform-browser';
-import { bootstrapApplication } from '@angular/platform-browser';
-import type { ApplicationRef } from '@angular/core';
-
-import { AppComponent } from './app/app.component';
-import { config } from './app/app.config.server';
-
-const bootstrap: (context: BootstrapContext) => Promise = (
- context: BootstrapContext,
-): Promise => bootstrapApplication(AppComponent, config, context);
-
-export default bootstrap;
diff --git a/src/server.ts b/src/server.ts
deleted file mode 100644
index c7ebdee..0000000
--- a/src/server.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { APP_BASE_HREF } from '@angular/common';
-import { CommonEngine, isMainModule } from '@angular/ssr/node';
-import type { Express, NextFunction, Request, Response } from 'express';
-import express from 'express';
-import { dirname, join, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-import bootstrap from './main.server';
-
-const serverDistFolder: string = dirname(fileURLToPath(import.meta.url));
-const browserDistFolder: string = resolve(serverDistFolder, '../browser');
-const indexHtml: string = join(serverDistFolder, 'index.server.html');
-const allowedHosts: readonly string[] = [
- 'fraujulian.xyz',
- 'www.fraujulian.xyz',
- 'test.fraujulian.xyz',
- 'localhost',
- '127.0.0.1',
-];
-
-const app: Express = express();
-const commonEngine: CommonEngine = new CommonEngine({ allowedHosts });
-
-app.use(
- express.static(browserDistFolder, {
- maxAge: '1y',
- index: 'index.html',
- }),
-);
-
-app.get('/{*splat}', (req: Request, res: Response, next: NextFunction): void => {
- const { protocol, originalUrl, baseUrl, headers } = req;
-
- commonEngine
- .render({
- bootstrap,
- documentFilePath: indexHtml,
- url: `${protocol}://${headers.host}${originalUrl}`,
- publicPath: browserDistFolder,
- providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
- })
- .then((html: string): Response => res.send(html))
- .catch((err: Error): void => next(err));
-});
-
-if (isMainModule(import.meta.url)) {
- const port: number = 3000;
-
- app.listen(port, (): void => {
- console.log(`Listening on port ${port}`);
- });
-}
-
-export default app;
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 46e40f8..a0dcc37 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -1,9 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
- "outDir": "./dist/out-tsc/app",
+ "outDir": "./out-tsc/app",
"types": []
},
- "files": ["src/main.ts", "src/main.server.ts", "src/server.ts"],
- "include": ["src/**/*.d.ts"]
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts"]
}