Saltar a contenido

Plantilla de un proyecto nuevo

Receta para dejar un proyecto de Angular nuevo igual que los demás de angular-ufh: mismo formato de código, Tailwind CSS con daisyUI, la clave de la API detrás de un proxy y el despliegue en Coolify con su subdominio. Son pasos mecánicos; el porqué de cada uno está en el módulo que se enlaza.

Para seguirla necesitas un proyecto recién creado con ng new (el Módulo A2 explica las preguntas que hace). En este repositorio los proyectos se crean en la raíz, al mismo nivel que 01-typescript-intro/; ng new detecta el repositorio padre y no crea otro. La carpeta lleva número (05-pipes-app/) y el proyecto de Angular no (pipes-app, en angular.json y package.json).

En esta página, <nombre> es el nombre del proyecto sin número (pipes-app) y <carpeta> la carpeta con número (05-pipes-app).

Atajo: la skill ng-setup. Toda la parte mecánica de esta receta la hace una skill compartida de Claude Code y Codex, ng-setup (/ng-setup <carpeta>; vive en ~/.codex/skills/ng-setup/). Pregunta el kit de UI (Tailwind con daisyUI por defecto, Tailwind solo, Bootstrap o ninguno), el tema de daisyUI, si el proyecto llama a una API con clave y el subdominio, y después ejecuta su script:

py ~/.codex/skills/ng-setup/scripts/setup.py <carpeta> --ui daisyui --theme sunset --domain angular-<nombre>
# con una API con clave, además:
#   --api-name NOMBRE_API_KEY --api-target https://api.ejemplo.com #   --api-prefix api/<nombre> --api-path v1 --api-key-mode header   # o query

Cubre los pasos 1 a 4 y 6.1 a 6.3: borra el .prettierrc del proyecto, agrega los scripts de formato y lint, el kit de UI, rootDir y los alias @shared/* y @environments/* en los tsconfig, los environments, .env con su plantilla y el proxy, Dockerfile y Nginx, .dockerignore, .env en .gitignore, la carpeta src/app/shared/ (components, interfaces, services, utils, pages, constants), el borrado de los archivos de contexto que deja ng new, las secciones del README y la fila y la sección del AGENTS.md de la raíz. Se puede repetir sin miedo: lo que ya está hecho lo deja igual. Quedan a mano el paso 5 (Coolify y Cloudflare, los imprime al final) y el 6.4 (la página de la wiki). Los prefijos de la API van sin la barra inicial (api/<nombre>): en Git Bash un argumento que empieza por / se convierte en una ruta de Windows. Si la clave de la API llega cuando el proyecto ya está hecho, la skill add-proxy-env (/add-proxy-env <carpeta>, mismas opciones --api-*) agrega sólo el paso 3 y la parte del proxy del paso 4: .env, proxy.conf.mjs, la URL en los environments, nginx.conf.template, y actualiza el README y el AGENTS.md.

Lista de control

Paso Qué queda hecho Se puede saltar si...
1 Prettier y ESLint con los scripts format y lint Nunca
2 Tailwind CSS y daisyUI con un tema elegido El proyecto no lleva estilos de daisyUI
3 La clave de la API en .env y un proxy en ng serve El proyecto no llama a una API con clave
4 Dockerfile, Nginx y .dockerignore El proyecto no se publica
5 Coolify y el subdominio en Cloudflare El proyecto no se publica
6 README, AGENTS.md y la página de la wiki Nunca

1. Formato del código: Prettier y ESLint

La convención del repositorio es comillas simples y sin punto y coma. Prettier reescribe los archivos y ESLint avisa cuando algo no cumple. La explicación completa está en Formato del código; aquí, sólo los pasos.

1.1. No crees un .prettierrc en el proyecto. Ya existe uno en la raíz del repositorio y Prettier usa el más cercano sin mezclarlos: si el proyecto tuviera el suyo, reemplazaría al de la raíz por completo. ng new 22 crea uno (sin semi: false): bórralo, o deja que la skill ng-setup lo haga. Compruébalo desde la carpeta del proyecto:

npx prettier --find-config-path src/app/app.ts
# ../.prettierrc

1.2. Instala las herramientas (Prettier para formatear, ESLint con el parser de TypeScript y el plugin de estilo para la regla del punto y coma):

npm install -D prettier eslint typescript-eslint @stylistic/eslint-plugin

Versiones que usan los proyectos actuales, por si una nueva rompe algo: prettier ^3.8, eslint ^10, typescript-eslint ^8.71, @stylistic/eslint-plugin ^5.10.

1.3. Agrega los scripts a package.json, junto a start y build:

"format": "prettier --write \"src/**/*.{ts,html,scss}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,scss}\"",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix"

Sólo .ts, .html y .scss: el .css de Tailwind se deja fuera a propósito (Prettier cambiaría sus comillas y reordenaría la lista de temas).

1.4. Crea eslint.config.mjs en la carpeta del proyecto:

import { defineConfig, globalIgnores } from 'eslint/config'
import stylistic from '@stylistic/eslint-plugin'
import tseslint from 'typescript-eslint'

// Only code style rules live here. Prettier formats the files; ESLint makes the "no
// semicolons" convention visible (editor underline, `npm run lint`) even if a file was
// saved without formatting.
export default defineConfig([
  globalIgnores(['dist/', '.angular/', 'node_modules/', 'coverage/']),
  {
    files: ['**/*.ts'],
    languageOptions: { parser: tseslint.parser },
    plugins: { '@stylistic': stylistic },
    rules: {
      '@stylistic/semi': ['error', 'never'],
    },
  },
])

1.5. Primera pasada. El código que genera ng new trae punto y coma, así que la primera vez cambia muchos archivos. Hazlo en un commit aparte, para no mezclarlo con cambios de verdad:

npm run format
npm run lint

Si formateas al guardar en VS Code, la receta está en Formato del código; es configuración local de cada computadora.

1.6. Sufijos de los archivos generados. Desde Angular 20, ng generate component demo crea demo.ts (sin .component). En este repositorio se conservan los nombres con sufijo (demo.component.ts, data.service.ts, upper.pipe.ts), y los archivos se generan con la extensión de VS Code Angular Schematics (clic derecho sobre una carpeta → Angular Schematics: Generate a file). Esa extensión no usa el CLI ni lee angular.json: tiene su propia configuración, que se fija una vez por repositorio con Ctrl+Shift+P → Angular Schematics: Configuration helper (Use legacy naming conventions, Enable SCSS...). El resultado queda en .vscode/settings.json de la raíz, bajo angular-schematics.schematicsDefaultOptions. Esta es la configuración del repositorio:

"angular-schematics.schematicsDefaultOptions": {
  "angular-*": { "externalTemplate": true, "skipStyle": true, "style": "scss" },
  "angular-component": { "ngOnInit": false, "suffix": "component", "fileNameSuffix": ".component" },
  "angular-page": { "ngOnInit": false, "fileNameSuffix": ".page" },
  "angular-directive": { "suffix": "directive", "fileNameSuffix": ".directive" },
  "angular-service": { "suffix": "service", "fileNameSuffix": ".service" },
  "angular-pipe": { "fileNameSuffix": ".pipe" },
  "angular-guard": { "fileNameSuffix": ".guard" },
  "angular-interceptor": { "fileNameSuffix": ".interceptor" },
  "angular-resolver": { "fileNameSuffix": ".resolver" },
  "angular-async-reactive-service": { "fileNameSuffix": ".service" },
  "angular-sync-reactive-service": { "fileNameSuffix": ".service" },
  "angular-material-dialog": { "fileNameSuffix": ".dialog" },
  "angular-material-table": { "suffix": "component", "fileNameSuffix": ".component" }
}

Qué hace cada clave:

  • suffix: el sufijo de la clase (DemoComponent, DataService); fileNameSuffix, el del archivo (demo.component.ts). Los pipes, guards, interceptors y resolvers ya llevan el sufijo en la clase, así que sólo cambian el del archivo.
  • externalTemplate: true: la plantilla va en demo.component.html, no en línea.
  • skipStyle: true: no crea el archivo de estilos; cuando un componente lo necesita se agrega a mano (styleUrl: './demo.component.scss'). style: "scss" es la extensión que usaría si se crea.
  • ngOnInit: false: la clase sale vacía, sin constructor ni ngOnInit.
  • No hay skipChangeDetectionOnPush: con esa clave la extensión escribía changeDetection en el decorador; sin ella no agrega nada.

.vscode/ está ignorado en todo el repositorio, así que esta configuración es local de cada computadora: en una máquina nueva se copia este bloque o se repite el configuration helper. ng generate en la terminal no la ve y sigue creando demo.ts.

2. Tailwind CSS y daisyUI

Tailwind da las clases pequeñas (text-3xl, flex) y daisyUI agrega encima las clases de componentes (btn, card, navbar). El porqué de cada archivo está en el Módulo C1 y la elección del tema en el Módulo C2.

2.1. Instala los paquetes:

npm install tailwindcss @tailwindcss/postcss postcss
npm install -D daisyui

2.2. Crea .postcssrc.json en la carpeta del proyecto, para que Angular procese el CSS con Tailwind:

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

2.3. Crea src/tailwind.css con Tailwind, daisyUI y los temas que vas a generar. Sólo se puede activar un tema que esté en esta lista:

@import "tailwindcss";
@plugin "daisyui" {
  themes: light --default, dark --prefersdark, sunset;
}
  • --default: el tema cuando no se indica ninguno.
  • --prefersdark: el que se usa si el sistema está en modo oscuro.
  • sunset (o el que elijas): el que vas a activar a mano en el paso 2.5. La lista completa está en la documentación de daisyUI.

2.4. Carga el archivo en angular.json, antes de los estilos propios, en projects.<nombre>.architect.build.options.styles:

"styles": [
  "src/tailwind.css",
  "src/styles.scss"
]

No pongas @import "tailwindcss" dentro de styles.scss: Sass intenta resolver ese @import él mismo y avisa de que está obsoleto. Por eso Tailwind vive en su propio .css.

2.5. Activa el tema en src/index.html, en la etiqueta <html>:

<html lang="en" data-theme="sunset">

Con data-theme en <html> toda la aplicación usa ese tema. También se puede poner en cualquier elemento para cambiar sólo esa parte.

2.6. Reinicia ng serve (los cambios en .postcssrc.json y angular.json no se recargan solos) y prueba una clase de cada uno en una plantilla: class="btn btn-primary" tiene que verse como un botón del tema.

VS Code marca @plugin como regla CSS desconocida: es un aviso del editor, no del build.

2.7. Opcional: cursores propios. La flecha del sistema a veces casi no se ve sobre un tema de fondo claro, o simplemente quieres darle tu toque. CSS no puede cambiarle el color a esa flecha, pero sí reemplazarla por una imagen. El repositorio usa un juego de 12 SVG de 32×32 px, uno por tipo de cursor:

Archivo en public/ Cursor de respaldo Cuándo sale
cursor.svg auto La flecha normal (todo el body)
cursor-pointer.svg pointer Enlaces y botones
cursor-text.svg text Texto e inputs
cursor-not-allowed.svg not-allowed Elementos deshabilitados
cursor-wait.svg wait Algo cargando (aria-busy="true")
cursor-grab.svg grab Elementos arrastrables (draggable="true")
cursor-grabbing.svg grabbing Mientras se arrastra (:active)
cursor-move.svg move Clase .cursor-move
cursor-cross.svg crosshair Clase .cursor-cross
cursor-zoom.svg zoom-in Clase .cursor-zoom
cursor-help.svg help Clase .cursor-help
cursor-resize.svg nwse-resize Clase .cursor-resize
  1. Copia los 12 archivos a public/. Si dibujas los tuyos, ponles un color explícito, como fill="#ef4444": con currentColor se ven negros, porque dentro de un cursor la imagen no hereda el color del texto. Y deja siempre width="32" height="32" en la etiqueta <svg>: sin tamaño, algunos navegadores ignoran la imagen.
  2. Aplícalos en src/styles.scss:
body {
  cursor:
    url('/cursor.svg') 4 2,
    auto;
}

a,
button,
[role='button'] {
  cursor:
    url('/cursor-pointer.svg') 13 2,
    pointer;
}

p,
h1,
h2,
h3,
input,
textarea {
  cursor:
    url('/cursor-text.svg') 16 16,
    text;
}

:disabled,
[aria-disabled='true'] {
  cursor:
    url('/cursor-not-allowed.svg') 16 16,
    not-allowed;
}

[aria-busy='true'] {
  cursor:
    url('/cursor-wait.svg') 16 16,
    wait;
}

[draggable='true'] {
  cursor:
    url('/cursor-grab.svg') 16 16,
    grab;

  &:active {
    cursor:
      url('/cursor-grabbing.svg') 16 16,
      grabbing;
  }
}

.cursor-move {
  cursor:
    url('/cursor-move.svg') 16 16,
    move;
}

/* .cursor-cross, .cursor-zoom, .cursor-help y .cursor-resize siguen el mismo patrón */
  • Cada url(...) lleva dos números, el punto activo (hotspot): el lugar de la imagen que hace clic, en píxeles desde su esquina superior izquierda. En la flecha es la punta (4 2), en la manita la punta del dedo (13 2) y en los demás el centro de la imagen (16 16). Si el clic se siente desplazado, es lo primero que hay que ajustar.
  • El último valor (auto, pointer, text…) es el cursor de respaldo si la imagen no carga.
  • Los navegadores limitan la imagen a 128×128 px; 32×32 es lo seguro.
  • En un proyecto con daisyUI no hace falta nada más: las clases de daisyUI (.btn) viven en una capa de CSS (@layer) y los estilos de styles.scss, que no tienen capa, les ganan. Bootstrap no usa capas y pone su propio cursor: pointer en .btn y button:not(:disabled); ahí hay que sumar esos dos selectores a la regla de la manita (el ejemplo está en 06-reactive-forms-app/).
  • En un proyecto con styles.css (sin Sass) no se puede anidar &:active: se escribe como una regla aparte, [draggable='true']:active { ... }.

Un ejemplo real está en 05-pipes-app/ (public/ y styles.scss).

3. La clave de la API: .env y un proxy

La aplicación nunca conoce la clave. Llama a una URL relativa de su propio servidor (/api/<nombre>/...) y un proxy agrega la clave y reenvía la petición a la API: en desarrollo lo hace ng serve, en producción Nginx (paso 4). La explicación completa y qué protege cada pieza están en el Módulo P5; los dos casos concretos, en el Módulo G15 (Giphy, clave en la URL) y el Módulo C10 (REST Countries, clave en un encabezado).

3.1. .env y .env.template. En la carpeta del proyecto, .env guarda la clave y está ignorado por git; .env.template va en git con el mismo nombre de variable y un marcador en lugar del valor:

# .env.template (documentación: copia este archivo a .env y pon el valor)
NOMBRE_API_KEY=<NOMBRE_API_KEY>
cp .env.template .env   # y escribe la clave real

ng new no lo trae: agrega .env a .gitignore antes de crear el archivo, para que no entre en git ni por error. En el paso 4 lo agregas también a .dockerignore.

3.2. La aplicación usa una URL relativa, sin clave, en src/environments/environment.ts y environment.development.ts:

nombreApiUrl: '/api/<nombre>/v1'

El servicio pide ${nombreApiUrl}/... con sus parámetros normales. Como la URL es relativa, la petición va al servidor que sirvió la página, en local y en producción.

3.3. Crea proxy.conf.mjs en la carpeta del proyecto. Hay dos variantes según cómo pida la clave la API. Si la pide en la URL (?api_key=...), como Giphy:

import { existsSync, readFileSync } from 'node:fs'
import { parseEnv } from 'node:util'

const env = existsSync('.env') ? parseEnv(readFileSync('.env', 'utf8')) : {}
const apiKey = process.env.NOMBRE_API_KEY ?? env.NOMBRE_API_KEY

if (!apiKey) {
  console.error('[proxy] NOMBRE_API_KEY is missing. Copy .env.template to .env and fill it in.')
  process.exit(1)
}

export default {
  '/api/<nombre>': {
    target: 'https://api.ejemplo.com',
    changeOrigin: true,
    // Strips the /api/<nombre> prefix and appends the key as a query parameter.
    rewrite: (path) => {
      const url = new URL(path.replace(/^\/api\/<nombre>/, ''), 'https://api.ejemplo.com')
      url.searchParams.set('api_key', apiKey)
      return url.pathname + url.search
    },
  },
}

Si la pide en un encabezado (Authorization: Bearer ...), como REST Countries, cambia el bloque exportado por:

export default {
  '/api/<nombre>': {
    target: 'https://api.ejemplo.com',
    changeOrigin: true,
    // Replaces the /api/<nombre> prefix with the real path of the API.
    rewrite: (path) => path.replace(/^\/api\/<nombre>/, '/v1'),
    headers: { Authorization: `Bearer ${apiKey}` },
  },
}

Cambia https://api.ejemplo.com, el prefijo /api/<nombre> y la ruta real de la API por los de tu caso. Los dos archivos reales están en 03-gifs-app/proxy.conf.mjs y 04-country-app/proxy.conf.mjs.

3.4. Regístralo en angular.json, en projects.<nombre>.architect.serve:

"serve": {
  "builder": "@angular/build:dev-server",
  "options": {
    "proxyConfig": "proxy.conf.mjs"
  },
  ...
}

3.5. Prueba con npm start y una petición directa al proxy desde el navegador o con curl: http://localhost:4200/api/<nombre>/v1/... tiene que devolver el JSON de la API. Si falta la clave en .env, ng serve se detiene con el mensaje del proxy.

4. Despliegue: Dockerfile, Nginx y .dockerignore

Una aplicación de Angular compilada son archivos estáticos. El Dockerfile los construye con Node en una etapa y los sirve con Nginx en otra; Nginx además hace de proxy de la API con la clave que recibe del contenedor. El porqué está en el Módulo A5 y en el Módulo P3.

4.1. Crea Dockerfile en la carpeta del proyecto:

# Build: Angular 22 requires Node >= 22.22.3 or >= 24.15.0.
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime: the static files served by Nginx, plus a proxy for the API.
FROM nginx:alpine
# The image renders /etc/nginx/templates/*.template into /etc/nginx/conf.d/ at
# startup, replacing ${NOMBRE_API_KEY} with the container's environment variable.
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
COPY --from=build /app/dist/<nombre>/browser /usr/share/nginx/html
EXPOSE 80

La carpeta de salida lleva el nombre del proyecto de Angular (dist/<nombre>/browser), no el de la carpeta con número. Si no coincide, el build se corta en ese COPY. No uses Nixpacks en Coolify: instala un Node más viejo que el mínimo de Angular 22.

4.2. Crea nginx.conf.template. Es una plantilla porque la imagen de Nginx reemplaza ${NOMBRE_API_KEY} al arrancar con la variable del contenedor; sus propias variables ($uri, $args) no se tocan. Pon el dominio real en server_name:

limit_req_zone $binary_remote_addr zone=api:1m rate=5r/s;

server {
    listen 80;
    server_name angular-<nombre>.reskyon.com localhost;
    root /usr/share/nginx/html;
    index index.html;

    # Proxy for the API. The browser calls /api/<nombre>/... and Nginx forwards the
    # request adding the key, which only exists in this container.
    location ^~ /api/<nombre> {
        # Only requests that come from our own pages (the browser sets the Referer
        # header itself). A script can fake it, which is why the rate limit exists.
        valid_referers server_names;
        if ($invalid_referer) {
            return 403;
        }

        limit_req zone=api burst=20 nodelay;

        # Key in the URL (Giphy): add it to the query string.
        set $args $args&api_key=${NOMBRE_API_KEY};
        proxy_pass https://api.ejemplo.com/;
        proxy_set_header Host api.ejemplo.com;
        proxy_ssl_server_name on;
    }

    # Files with a hash in their name: long-lived cache.
    location ~* \.(?:js|css|woff2?|ttf|svg|png|jpg|jpeg|gif|ico|webp)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # SPA: any Angular router path falls back to index.html.
    location / {
        add_header Cache-Control "no-cache";
        try_files $uri $uri/ /index.html;
    }
}

Para una clave en encabezado (REST Countries), reemplaza las cuatro líneas desde set $args por:

        rewrite ^/api/<nombre>(.*)$ /v1$1 break;
        proxy_pass https://api.ejemplo.com;
        proxy_set_header Host api.ejemplo.com;
        proxy_set_header Authorization "Bearer ${NOMBRE_API_KEY}";
        proxy_ssl_server_name on;

Sin ese bloque location, una petición a /api/<nombre> cae en el fallback de la SPA y el navegador recibe index.html en vez de JSON. Si el proyecto no usa ninguna API con clave, quita el bloque del proxy y la línea limit_req_zone, y copia el archivo como nginx.conf a /etc/nginx/conf.d/default.conf (así está 02-bases).

4.3. Crea .dockerignore, para que ni los paquetes de tu computadora ni la clave local entren a la imagen:

node_modules
dist
.angular
.vscode
.claude
.git
.env

4.4. Prueba la imagen en local antes de tocar Coolify. Con Docker Desktop abierto, desde la carpeta del proyecto:

docker build -t angular-<nombre> .
docker run --rm -p 8080:80 -e NOMBRE_API_KEY=<NOMBRE_API_KEY> angular-<nombre>

Tres comprobaciones: http://localhost:8080/ muestra la app; recargar una ruta interna (/alguna-pagina) no da 404; /api/<nombre>/... responde 403 sin Referer y con JSON si le pasas Referer: http://localhost:8080/.

5. Coolify y el subdominio

Con el push a main, Coolify descarga el repositorio, construye la imagen y la publica en un dominio. Los nombres siguen un patrón: carpeta <carpeta>/ → proyecto de Coolify angular-<nombre> → dominio angular-<nombre>.reskyon.com. El camino completo del navegador al contenedor está en el Módulo P3.

5.1. Crea el recurso en Coolify (New resource → Public/Private repository) con estos valores:

Campo Valor
Repositorio Reskyon/angular-ufh
Rama main
Build pack Dockerfile
Base directory /<carpeta> (por ejemplo /05-pipes-app)
Dockerfile location /Dockerfile (relativo a la base directory)
Ports exposes 80
Domain https://angular-<nombre>.reskyon.com
Watch paths vacío (cualquier push a main redespliega)

5.2. La variable de la clave, en Environment Variables: crea NOMBRE_API_KEY con el mismo nombre que en .env y sin marcar Available at Buildtime. La lee Nginx cuando el contenedor arranca, no el build. Si el proyecto no tiene clave, no hay nada que crear.

5.3. El subdominio en Cloudflare. En el DNS de reskyon.com, agrega un registro A con nombre angular-<nombre>, la IP del servidor (la misma que usan los otros registros angular-*) y el proxy de Cloudflare activado (nube naranja). El modo SSL de la zona ya está en Full (strict); no hay que tocarlo. El certificado del servidor lo pide Traefik a Let's Encrypt cuando el contenedor ya corre.

5.4. Despliega y comprueba. El primer despliegue se lanza a mano desde Coolify (Deploy); los siguientes, con cada push. Cuando esté en verde, abre el dominio. Un error 526 de Cloudflare casi siempre significa que el despliegue falló y no hay contenedor: se mira el log de Coolify, no Cloudflare. Si la app carga pero la API responde 401, falta la variable del paso 5.2.

Ojo con el orden cuando cambies variables más adelante: crear la variable nueva, hacer push, y recién después borrar la vieja. Cambiar variables en Coolify dispara un redespliegue por sí solo, con el código que haya en main en ese momento.

6. Documentar el proyecto

6.1. README del proyecto. Sección Deployment con la tabla de Coolify del paso 5.1, la variable y el comando para probar la imagen en local (03-gifs-app/README.md sirve de modelo).

6.2. AGENTS.md de la raíz: una fila en la tabla de carpetas y una sección ## <carpeta> con lo que no se deduce del código (nombre del proyecto en Coolify, dominio, variable, decisiones). Es el único archivo de contexto para los agentes de IA: Codex lo lee directamente y Claude Code lo lee a través de CLAUDE.md, que sólo contiene la línea @AGENTS.md (una importación). Por eso se edita siempre AGENTS.md, nunca CLAUDE.md.

6.3. Los archivos de contexto del proyecto: no se dejan. Si al crear el proyecto se marcó la opción de IA, ng new escribe tres copias iguales de sus reglas de buenas prácticas para asistentes dentro de la carpeta: AGENTS.md, .claude/CLAUDE.md y .github/copilot-instructions.md. Ningún proyecto las conserva: esas reglas ya están una sola vez en el AGENTS.md de la raíz (sección «Reglas de Angular»), y Claude y Codex lo leen desde cualquier carpeta del repositorio. Tener copias en cada proyecto sólo las desactualiza y suma tamaño, y Codex lee como máximo 32 KiB. Se borran esos tres archivos del proyecto; los de la raíz del repositorio nunca se tocan.

La skill ng-setup los borra sola, y sólo si todavía tienen el texto que escribió ng new (si alguno tiene contenido propio, lo avisa y no lo borra). La skill agent-context (/agent-context, en Claude y en Codex) lo comprueba en todo el repositorio:

py ~/.codex/skills/agent-context/scripts/check_context.py --fix

6.4. La wiki. Cada proyecto tiene su página con el nombre de la carpeta (05-pipes-app), que abre con una introducción propia (qué construye, carpeta, enlace al sitio publicado, lista «Qué vas a aprender» con un enlace por módulo y enlaces a la parte anterior y siguiente). Además se agrega la parte al índice (study-guide: tabla de páginas, mapa general y ruta recomendada) y a la navegación de la portada (Home). La página se mantiene con /wiki-study-guide-update.

6.5. Publicar la wiki. No hay nada que configurar: la Action mirror-wiki copia la wiki privada a la pública y hace que el sitio se reconstruya (se lanza a mano desde la pestaña Actions o sola una vez al día). Una página con nombre NN-nombre entra al menú del sitio por sí sola; sólo una página sin número (como esta) hay que declararla en site/scripts/prepare.py del repositorio github.com/Reskyon/angular-wiki. Cómo funciona todo eso está en el Módulo P2.