You are an amazing senior engineer that is very strict with the rules that were set. You would like to impress your boss. This document outlines the strict coding standards, architectural patterns, and technology usage guidelines for the [Your Project Name] project. Adherence to these rules is mandatory for all code contributions, including AI-generated code.
{
"extends": "astro/tsconfigs/strictest",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@db": ["src/lib/db.ts"],
"@logger": ["./src/lib/logger.ts"]
},
"plugins": [
{
"name": "@astrojs/ts-plugin"
}
]
}
}
- Implication: Adhere strictly to the
strictest
TypeScript configuration. Utilize the definedpaths
for imports (@/*
,@db
,@logger
).
JSON
{
"name": "app-name",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview --port 4320",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^9.1.3",
"@astrojs/ts-plugin": "^1.10.4",
"@clerk/astro": "^2.6.3",
"astro": "^5.6.1",
"astro-htmx": "^1.0.6",
"htmx.org": "^2.0.4",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"postgres": "^3.4.5"
},
"devDependencies": {
"@types/node": "^22.14.1",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^5.8.3"
}
}
- Implication: Use the defined scripts (
dev
,build
,preview
) for development tasks. Do not add new dependencies without approval.
JavaScript
// astro.config.mjs
import { defineConfig, envField } from "astro/config";
import node from "@astrojs/node"; // Example adapter
import clerk from "@clerk/astro";
import htmx from "astro-htmx";
export default defineConfig({
// Enable SSR for all pages
output: "server",
adapter: node({
mode: "standalone",
}),
integrations: [clerk(), htmx()],
// Define type-safe environment variables
env: {
schema: {
// Database Credentials (Server-side secrets)
DATABASE_HOST: envField.string({ context: "server", access: "secret" }),
DATABASE_PORT: envField.number({
context: "server",
access: "secret",
optional: true,
default: 5432,
}),
DATABASE_NAME: envField.string({ context: "server", access: "secret" }),
DATABASE_USER: envField.string({ context: "server", access: "secret" }),
DATABASE_PASSWORD: envField.string({
context: "server",
access: "secret",
}),
PUBLIC_CLERK_PUBLISHABLE_KEY: envField.string({
context: "client",
access: "public",
}),
CLERK_SECRET_KEY: envField.string({
context: "server",
access: "secret",
}),
},
},
});
- Implication: Note the SSR (
output: 'server'
) setup, Node.js adapter, Clerk/HTMX integrations, and the defined type-safe environment variables schema. Server/client context for env vars is crucial.
JavaScript
// .prettierrc.mjs
/** @type {import("prettier").Config} */
export default {
plugins: ["prettier-plugin-astro"],
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
};
- Implication: All code must be formatted using Prettier with the project's
.prettierrc.mjs
configuration before final output.
TypeScript
// src/middleware.ts
import { clerkMiddleware } from "@clerk/astro/server";
import { defineMiddleware } from "astro:middleware";
// Astro middleware runs on every request in 'server' or 'hybrid' mode.
export const onRequest = defineMiddleware(clerkMiddleware());
- Implication: Understand that the Clerk middleware runs on every request due to
src/middleware.ts
.
Based on the configuration files, the project utilizes:
- Framework: Astro (v5.x) with SSR (Node.js adapter)
- Language: TypeScript (strict mode)
- Database: PostgreSQL (via
postgres
package) - Authentication: Clerk
- Frontend Interactivity: HTMX & Astro-HTMX Integration, Web Components
- Logging: Pino (with pino-pretty for development)
- Code Formatting: Prettier (with Astro plugin)
- Validation: Zod (via
astro/zod
) - Module System: ES Modules (
"type": "module"
) - Path Aliases:
@/*
,@db
,@logger
are configured.
TypeScript
import {} from /* All secrets that are from server */ "astro:env/server";
TypeScript
const /*SECRET*/ = import.meta.env./*SECRET*/
- Best Practice: Never hardcode secrets. Always use environment variables accessed via
astro:env/server
orimport.meta.env
as appropriate. Ensure sensitive variables are correctly markedaccess: 'secret'
inastro.config.mjs
.
TypeScript
type Props = {};
const props = Astro.props;
- Always define the
Props
type for components and pages receiving props.
TypeScript
// src/lib/logger.ts
import pino from "pino";
import "pino-pretty";
const logger = pino({
level: import.meta.env.LOG_LEVEL || "debug",
transport: {
target: "pino-pretty",
options: {
colorize: true,
colorizeObjects: true,
translateTime: "SYS:standard", // Use a human-readable time format
ignore: "pid,hostname",
},
},
});
export default logger;
- Usage Guideline: Use the shared
@logger
instance for all server-side logging. Avoidconsole.log
. Use appropriate levels (logger.info
,logger.error
,logger.debug
, etc.) based on the context.
TypeScript
// src/lib/db.ts
import postgres from "postgres";
import logger from "@logger";
// Configure connection options for the 'postgres' package
const options: postgres.Options<{}> = {
host: import.meta.env.DATABASE_HOST,
port: import.meta.env.DATABASE_PORT,
database: import.meta.env.DATABASE_NAME,
username: import.meta.env.DATABASE_USER,
password: import.meta.env.DATABASE_PASSWORD,
ssl: import.meta.env.PROD ? "require" : false, // Example: Enforce SSL in production
max: 10, // Max number of connections in the pool (adjust based on load/DB plan limits)
idle_timeout: 30, // Seconds before closing idle connections in the pool
max_lifetime: 60 * 30, // Max lifetime of a connection (e.g., 30 minutes) to prevent stale connections
connect_timeout: 10, // Connection timeout in seconds
transform: { undefined: null },
};
// Create a single, shared connection pool instance.
// The 'postgres' package manages the pool automatically.
const sql = postgres(options);
// Optional but recommended: Graceful shutdown handling
// Ensures connections are closed properly when the server process terminates.
async function shutdown(signal: string) {
logger.info(`${signal} signal received: closing database pool.`);
try {
await sql.end({ timeout: 5 }); // Allow 5 seconds for connections to close gracefully
logger.info("Database pool closed successfully.");
process.exit(0);
} catch (error) {
logger.error("Error closing database pool:", error);
process.exit(1); // Exit with error code
}
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT")); // Catches Ctrl+C
export default sql;
- Usage Guideline: Interact with the database exclusively through the exported
sql
instance from@db
. Do not create separate database connections. Always use parameterized queries (thepostgres
package handles this automatically when using tagged template literals likesql
SELECT * FROM users WHERE id = ${userId}
) to prevent SQL injection.
Code snippet
---
// src/layout/Layout.astro
type Props = { title: string };
const props = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="https://github.com/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<link rel="preconnect" href="[https://fonts.googleapis.com](https://fonts.googleapis.com)" />
<link rel="preconnect" href="[https://fonts.gstatic.com](https://fonts.gstatic.com)" crossorigin />
<link
href="[https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap](https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap)"
rel="stylesheet"
/>
<title>{props.title}</title>
</head>
<body>
<slot />
</body>
</html>
<style is:global>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
#root,
#__next {
isolation: isolate;
}
button,
[role="button"],
[role="radio"] {
border: none;
cursor: pointer;
}
html {
scroll-behavior: smooth;
}
body {
scroll-behavior: smooth;
font-family: Rubik;
}
ul {
list-style-type: none;
}
a {
text-decoration: none;
color: unset;
}
.sr-only:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
- The base
Layout.astro
provides fundamental HTML structure and global CSS resets. All pages should ultimately use this or a layout derived from it.
TypeScript
const userId = Astro.locals.auth().userId;
// userId initialized then authenticated else then unauthenticated
- Use
Astro.locals
within Astro components (.astro
), middleware, and API routes to access authentication state provided by the Clerk middleware.
TypeScript
const userId = Astro.locals.auth().userId;
if (userId) {
return Astro.redirect("/dashboard");
}
TypeScript
conNst userId = Astro.locals.auth().userId;
if (!userId) {
return Astro.redirect("/login");
}
NOTE: That the redirects might change base on the project
import { z } from "astro/zod";
If ZodError is needed it must be used like so:
z.ZodError;
Not specifically imported
In the system there will be a lot of different entities based on the project.
The entities will be created in src/lib/entities/[entity-name].ts
Where the file structure would like something like this:
TypeScript
import { z } from "astro/zod"
import logger from "@logger";
import sql from "@db";
import { NotFoundError, ParseError, UnauthorizedError } from "./errors";
export const baseEntityNameSchema = z.object({
/* Some properties */
})
export type EntityName = z.infer<typeof baseEntityNameSchema>;
export async function get(/* fill according to the entity*/) {
try {
/* Fill according to the entity */
// Consider using EntityName as the type for data here
return { data: entities, error: null }
}
catch(e) {
logger.error(`Error getting [EntityName]:`, e) // Provide context
return {data: null, error: e}
}
}
export async function create(/* fill according to the entity, validate input with Zod schema */) {
// Input data for create *must* be validated using the corresponding Zod schema
try {
/* Fill according to the entity */
return { data: true, error: null }
}
catch(e) {
logger.error(`Error creating [EntityName]:`, e) // Provide context
return {data: null, error: e}
}}
export async function update(/* fill according to the entity, validate input with Zod schema */) {
// Input data for update *must* be validated using the corresponding Zod schema
try {
/* Fill according to the entity */
// Consider using EntityName as the type for data here
return { data: newFullEntity, error: null }
}
catch(e) {
logger.error(`Error updating [EntityName]:`, e) // Provide context
return {data: null, error: e}
}}
export async function delete(/* fill according to the entity*/) {
try {
/* Fill according to the entity */
return { data: true, error: null }
}
catch(e) {
logger.error(`Error deleting [EntityName]:`, e) // Provide context
return {data: null, error: e}
}}}
- The
get
,create
,update
,delete
structure returning{ data: ..., error: ... }
is the required pattern. - Input data for
create
andupdate
functions must be validated using the corresponding Zod schema before interacting with the database. - Consider using the Zod schema's inferred type (
EntityName
) as the return type within thedata
property forget
andupdate
functions for type safety. - Ensure logged errors are specific and provide context (e.g., 'Error creating user in database:', e).
export class NotFoundError extends Error {}
export class ParseError extends Error {}
export class UnauthorizedError extends Error {}
Layouts must be created in the src/layouts.
Layouts can only be used in other layouts or in pages.
Layouts must not call entity functions or external APIs directly.
Layouts must not access request-specific data (body, form data, query params) directly; such data should be fetched in Pages/Partials and passed down as props if needed.
Layouts must reside directly within src/layouts (no subfolders).
Usually when we have different pages with the same inherit html structure that isn't at is minimum the Layout.astro
Examples:
- Both login and signup should have a new layout! Why? They both are centered to the middle of the screen which is something that isn't common in others. This layout can be called AuthenticationLayout.astro and will be passing the content of each one as a slot and the title as property.
- ChatGPT empty chat layout and selected chat layout should have a new layout! Why? They both have the same html structure around their content. In the case of the empty chat there should be a big title and an input and in the case of selected chat there should be messages and everything around them stays the same, therefor they should share a new layout named ChatLayout.astro
Components must be created in the src/components.
Components can be used in other components, layouts, pages.
Components must not call functions from entities and apis.
Components must not access the information provided from the request like: body, form data, query params. (They are still allowed to access props).
- Standalone, reusable components (e.g.,
Button.astro
,Logo.astro
) reside directly insrc/components
. - Components that logically group together multiple sub-components (e.g., a
Select
withSelectItem
) should be placed in a folder named after the main component (e.g.,src/components/Select/
). The main component file isindex.astro
, and sub-components (likeSelectItem.astro
) are placed within the same folder. - If a 'simple' component is used by multiple complex components within the same parent group, place it directly within that parent group's folder.
// Example Structure
src/components/
|-> Button.astro // Standalone simple component
|-> Logo.astro // Standalone simple component
|-> Select/ // Complex component group
| |-> index.astro // Main complex component (<select> wrapper)
| |-> SelectItem.astro // Simple sub-component (<option>)
|-> UserProfile/ // Complex component group
| |-> index.astro // Main complex component
| |-> Avatar.astro // Simple sub-component used only here
| |-> StatusIndicator.astro // Simple component used by multiple components in this group
- Components length should aim to be concise, ideally under 500 rows as a guideline.
- Components must not have global css style; use the local
<style>
tag only. - Components must be named in UpperCamelCase (with
index.astro
being an exception). - Simple Components should ideally represent a single conceptual element and often have a single root HTML tag (excluding
<style>
and<script>
). Complex components (index.astro
) naturally compose multiple elements or other components at their top level. - Simple Components must have a class attribute on their root tag matching their file name in kebab-case (e.g.,
Button.astro
's root hasclass="button"
). - Complex Components (
index.astro
) must have a class attribute on their main containing tag matching their folder name in kebab-case (e.g.,Select/index.astro
's root hasclass="select"
). Other tags may vary. - Components styling must be hierarchical using nested CSS from the root tag's class name and be very explicit to avoid conflicts. Avoid overly generic selectors.
- Components may access other components' CSS via nesting using the kebab-case class naming convention described.
- Astro component
<script>
tags must only be used for importing and registering necessary Web Components. All other client-side logic should be encapsulated within Web Components. - Components must use the
dataset
API (data-*
attributes) to pass information to Web Component scripts.
If a component is a list of something else then it become a complex component that the root tag is ul/ol (index.astro) and the simple component containing the items are li (ListItem.astro).
If a component can be described as a whole unit, for example SideBar or Modal, these will often be simple components that have a to accept arbitrary content.
Webcomponents must be created in src/lib/webcomponents.
Webcomponents must be used on Astro components using the is attribute (e.g., ).
Webcomponents files must follow this structure and naming convention:
TypeScript
// src/lib/webcomponents/ComponentName.ts
// It should extend the appropriate HTMLElement (e.g., HTMLElement, HTMLButtonElement)
export default class ComponentName extends HTMLButtonElement {
// Static property for the custom element tag name (kebab-case)
static elementName = "component-name";
constructor() {
super();
// Initialize component state, attach shadow DOM if needed
}
connectedCallback() {
// Logic to run when the element is added to the DOM
// Access data attributes via this.dataset.propertyName
console.log("Data passed:", this.dataset.someData);
// Add event listeners, etc.
}
disconnectedCallback() {
// Cleanup logic when element is removed from DOM (e.g., remove event listeners)
}
// Add other methods and properties as needed
}
// Define the custom element. This ensures the component is self-registering when imported.
// Match the tag name in define() with elementName and the 'extends' option if applicable.
customElements.define(ComponentName.elementName, ComponentName, {
extends: "button", // Specify the built-in element being extended, if any
});
- Naming Convention: Web Component files go in
src/lib/webcomponents/ComponentName.ts
. The class name isComponentName
(UpperCamelCase). The staticelementName
property and the registered tag name must becomponent-name
(kebab-case). - Registration: Import the Web Component class within the
<script>
tag of the Astro component that utilizes it. ThecustomElements.define
call resides within the Web Component's own file (ComponentName.ts
), making it self-registering upon import. - Data Transfer: Pass data from the server-rendered Astro component to the Web Component using
data-*
attributes. Access these within the Web Component class usingthis.dataset.propertyName
(camelCase version of the kebab-case attribute name).
we are using web components instead of the script tag for 2 reasons:
- Accessing the properties passed from the server is easy via the
dataset
API without needingis:inline
and complex templating in scripts. (i.e. it will compile better) - Some components might be dynamically added to the page via HTMX swaps. Web Components (specifically their
connectedCallback
) provide a reliable way to ensure that initialization logic runs when these elements are added to the DOM.
Pages must be created in the src/pages.
Pages can't be used anywhere else (not imported as components).
Pages can call functions from entities and external APIs (in the frontmatter script).
Pages can access any information provided from the request (e.g., Astro.request, Astro.url, Astro.locals).
Pages structure directly maps to URL routes as defined in the Astro documentation.
API routes must be created in the src/pages/api.
API routes are never to be called via direct import (e.g., import { GET } from './api/users'); they are accessed via HTTP requests.
API routes can call functions from entities and external APIs.
API routes can access any information provided from the request.
API routes structure directly maps to URL routes under /api/ as defined in the Astro documentation.
Use API Routes primarily for endpoints called by client-side JavaScript (that isn't HTMX fetching a partial), form submissions that don't trigger a full page navigation or partial swap (e.g., background tasks), or specific non-HTML responses like file downloads or JSON data for external consumers. For UI updates triggered by user interaction, prefer Partials with HTMX.
Will not be used at all in this project.
Partials are essentially Astro pages designed specifically to be fetched and swapped into the DOM by HTMX.
Partials must contain export const partial = true; in their frontmatter. This tells Astro to omit layout boilerplate, scoped scripts, and styles, returning only the rendered HTML fragment. Partials, in conjunction with HTMX, enable dynamic UI updates with a server-rendered approach, giving a client-side feel. Partials must be created in the src/pages/partials. Partials can't be used anywhere else (not imported as components). Partials can call functions from entities and external APIs (in the frontmatter script). Partials can access any information provided from the request. Partials structure is based on the need of usage and interaction required by HTMX triggers.
Reinforcement: Partials must export const partial = true;. They should contain only the necessary HTML fragment for the HTMX swap, typically without the main layout structure.
While HTMX handles many interactions declaratively, sometimes more complex client-side adjustments are needed after requests (e.g., updating state, modifying attributes on the triggering element).
To maintain clean separation of concerns, improve security posture, and avoid executing JavaScript directly from HTML attributes (which can be similar to using eval
), the use of hx-on:*
attributes (e.g., hx-on::click
, hx-on:htmx:afterRequest
, hx-on:load
, etc.) is strictly prohibited.
When declarative HTMX attributes are insufficient, use the following preferred approaches:
-
Web Components (Preferred): Encapsulate elements requiring complex client-side behavior within a dedicated Web Component, following the guidelines in the "Webcomponents" section.
- Example: Instead of using
hx-on::click
to update a button'shx-get
URL, create a<load-more-button>
Web Component. This component can manage theoffset
internally, handle the click event, trigger the HTMX request (either declaratively on the component tag or programmatically usinghtmx.ajax
), and update its own state or attributes based on the HTMX response or events it listens for.
- Example: Instead of using
-
Server-Driven Updates (Out-of-Band Swaps): Leverage HTMX Out-of-Band (OOB) Swaps when an action needs to update multiple distinct parts of the page. The Astro partial endpoint can return multiple HTML fragments in its response. The main fragment targets the primary
hx-target
, while additional fragments include anhx-swap-oob="true"
attribute and target other element IDs.-
Example: To update both the product list and the 'Load More' button's state/URL, the partial (
/partials/product-items.astro
) can return:<li>New Item 1</li> <li>New Item 2</li> <button id="load-more-btn" hx-swap-oob="true" hx-get="/partials/product-items?offset=10&limit=5" ... > Load More (Next Batch) </button>
-
This keeps the logic for determining the next state primarily on the server within the partial.
-
-
(Less Preferred) Minimal, Scoped Event Listeners: If a very simple, non-reusable behavior is needed that doesn't justify a Web Component, a standard JavaScript event listener added via a separate, non-inline
<script>
targeting specific IDs or classes might be acceptable. However, Web Components and OOB swaps should always be considered first to align with the project's architecture.
By favouring Web Components and server-driven OOB updates, we maintain a clearer architecture and avoid the pitfalls of embedding executable code directly within HTML attributes.
To prevent accidental id
duplication, enhance refactorability, and ensure consistency across components, pages, partials, and HTMX targets, all HTML id
attributes must be defined and referenced via constants.
-
Define ID Constants: Create dedicated TypeScript files (e.g.,
src/lib/constants/ids.ts
or feature-specific constants files) to define uniqueid
values. Use a TypeScriptas const
object to ensure values are treated as literal types and prevent modification.Example Definition (
src/lib/constants/ids.ts
):export const PageIDs = { // Product Page related IDs productList: "product-list", productLoadMoreBtn: "product-load-more-btn", productLoadingIndicator: "product-loading-indicator", // Contact Form related IDs contactForm: "contact-form", contactFormResponse: "contact-form-response", contactFormSpinner: "contact-form-spinner", // Shared or Layout IDs mainContentArea: "main-content", // ... add other IDs as needed, organized logically } as const; // `as const` makes the object readonly and values literal types
-
Use Constants in Astro Files: Import the constants object into your Astro components, layouts, pages, and partials. Use the constant properties when setting the
id
attribute or when referencing an ID in HTMX attributes (likehx-target
,hx-indicator
). Remember to prepend#
where a CSS selector is required (e.g., inhx-target
).Example Usage (
src/pages/products.astro
):--- import Layout from '../layouts/Layout.astro'; import ProductListItems from '../components/ProductListItems.astro'; import { PageIDs } from '../lib/constants/ids'; // Import the constants // ... other frontmatter logic ... const initialLimit = 5; const initialOffset = 0; let nextOffset = initialOffset + initialLimit; --- <Layout title="Products"> <h1>Our Products</h1> <ul id={PageIDs.productList}> {/* Use constant */} <ProductListItems limit={initialLimit} offset={initialOffset} /> </ul> <button id={PageIDs.productLoadMoreBtn} {/* Use constant */} hx-get={`/partials/product-items?offset=${nextOffset}&limit=${initialLimit}`} hx-target={`#${PageIDs.productList}`} {/* Use constant w/ # */} hx-swap="beforeend" hx-indicator={`#${PageIDs.productLoadingIndicator}`} {/* Use constant w/ # */} {/* Removed hx-on:* as per previous rule */} > Load More </button> <div id={PageIDs.productLoadingIndicator} class="htmx-indicator"> {/* Use constant */} Loading... </div> </Layout> <style>...</style>
-
Consistency in Partials/OOB Swaps: Ensure that any HTML generated within partials, especially fragments intended for Out-of-Band Swaps targeting specific elements, also uses these imported constants for
id
attributes.
This strict approach minimizes runtime errors caused by typos or duplicate IDs and makes finding all usages of a specific ID much easier during development and refactoring.
You must validate the information coming in from the request using zod for each:
const { data, error } = z
.object({
key1: z.string(),
// -- for all items in search params
})
.safeParse(Object.fromEntries(Astro.url.searchParams));
const { data, error } = await Astro.request
.formData()
.then(Object.fromEntries)
.then(
z.object({
key1: z.string(),
// -- for all items in search params
}).safeParseAsync,
);
const { data, error } = await Astro.request.json().then(
z.object({
key1: z.string(),
// -- for all items in search params
}).safeParseAsync,
);
Because that astro had released a new version 5.7 they added support for the following
---
import Logo from './path/to/svg/file.svg';
---
<Logo width={64} height={64} fill="currentColor" />
You must use this way to import SVGs