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/) -image +Portfolio website preview -## 👂 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"] }