mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
Compare commits
3 Commits
ff6e988125
...
5e1324adf5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e1324adf5 | ||
|
|
86a9dc2d31 | ||
|
|
e38756d521 |
153
AGENTS.md
Normal file
153
AGENTS.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Agent Guidelines for this Repository
|
||||||
|
|
||||||
|
> **Important**: This file is the source of truth for all agents working in this repository. If you modify the build process, add new tools, change the architecture, or introduce new conventions, **you must update this file** to reflect those changes.
|
||||||
|
|
||||||
|
## 1. Project Overview & Commands
|
||||||
|
|
||||||
|
This is an **Astro** project using **pnpm**.
|
||||||
|
|
||||||
|
### Build & Run Commands
|
||||||
|
- **Install Dependencies**: `pnpm install`
|
||||||
|
- **Development Server**: `pnpm dev`
|
||||||
|
- **Build for Production**: `pnpm build` (Output: `dist/`)
|
||||||
|
- **Preview Production Build**: `pnpm preview`
|
||||||
|
- **Check Types**: `npx tsc --noEmit` (since `strict` is enabled)
|
||||||
|
|
||||||
|
### Testing & Linting
|
||||||
|
- **Linting**: No explicit lint command found in `package.json`. Follow existing code style strictly.
|
||||||
|
- **Testing**: No explicit test framework (Jest/Vitest) is currently configured.
|
||||||
|
- *If asked to add tests*: Verify if a test runner needs to be installed first.
|
||||||
|
|
||||||
|
## 2. Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/ # Source assets (processed by Astro)
|
||||||
|
│ ├── components/ # Astro components
|
||||||
|
│ │ ├── base/ # UI Primitives
|
||||||
|
│ │ └── page/ # Layout-specific components (Header, Footer)
|
||||||
|
│ ├── content/ # Markdown/MDX content collections
|
||||||
|
│ │ ├── posts/
|
||||||
|
│ │ ├── experiences/
|
||||||
|
│ │ └── skills/
|
||||||
|
│ ├── data/ # Data access layer (Classes & Utilities)
|
||||||
|
│ ├── layouts/ # Page layouts (if any)
|
||||||
|
│ ├── pages/ # File-based routing
|
||||||
|
│ ├── styles/ # Global styles (if any)
|
||||||
|
│ └── utils/ # Helper functions
|
||||||
|
├── astro.config.ts # Astro configuration
|
||||||
|
├── package.json # Dependencies & Scripts
|
||||||
|
└── tsconfig.json # TypeScript configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Code Style & Conventions
|
||||||
|
|
||||||
|
### General Formatting
|
||||||
|
- **Indentation**: 2 spaces.
|
||||||
|
- **Semicolons**: **Preferred**. While mixed in some older files, new code should use semicolons.
|
||||||
|
- **Quotes**:
|
||||||
|
- **Code**: Single quotes `'string'`.
|
||||||
|
- **Imports**: Double quotes `"package"` or single quotes `'./file'` (mixed, generally follow file context).
|
||||||
|
- **JSX/Attributes**: Double quotes `<Component prop="value" />`.
|
||||||
|
- **Line Endings**: LF.
|
||||||
|
|
||||||
|
### TypeScript & JavaScript
|
||||||
|
- **Strictness**: `strictNullChecks` is enabled via `astro/tsconfigs/strict`.
|
||||||
|
- **Path Aliases**:
|
||||||
|
- Use `~/` to refer to `src/` (e.g., `import { data } from '~/data/data'`).
|
||||||
|
- **Naming**:
|
||||||
|
- Components/Classes: `PascalCase` (e.g., `Header.astro`, `Posts`).
|
||||||
|
- Variables/Functions: `camelCase` (e.g., `getPublished`).
|
||||||
|
- Constants: `camelCase` or `UPPER_CASE`.
|
||||||
|
- Private Fields: Use JS private fields `#field` over TypeScript `private` keyword where possible.
|
||||||
|
- **Error Handling**: Use `try...catch` or explicit checks (e.g., `if (!entry) throw new Error(...)`).
|
||||||
|
|
||||||
|
### Astro Components (.astro)
|
||||||
|
- **Structure**:
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// Imports
|
||||||
|
import { Picture } from "astro:assets";
|
||||||
|
import { data } from "~/data/data";
|
||||||
|
|
||||||
|
// Logic (Top-level await supported)
|
||||||
|
const { title } = Astro.props;
|
||||||
|
const posts = await data.posts.getPublished();
|
||||||
|
---
|
||||||
|
<!-- Template -->
|
||||||
|
<div class="container">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{posts.map(post => <a href={post.slug}>{post.data.title}</a>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Scoped CSS */
|
||||||
|
.container {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
}
|
||||||
|
/* Nesting is supported and encouraged */
|
||||||
|
.parent {
|
||||||
|
.child { color: red; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
- **Images**: Use `<Picture />` from `astro:assets` for optimized images.
|
||||||
|
- **CSS**:
|
||||||
|
- Use scoped `<style>` blocks at the bottom of the file.
|
||||||
|
- **Variables**: Use CSS variables for theming (e.g., `var(--content-width)`, `var(--t-fg)`).
|
||||||
|
|
||||||
|
## 4. Architecture & Patterns
|
||||||
|
|
||||||
|
### Data Access Layer (`src/data/`)
|
||||||
|
The project uses a dedicated data access layer instead of querying collections directly in components.
|
||||||
|
|
||||||
|
- **Pattern**:
|
||||||
|
- Data logic is encapsulated in classes (e.g., `class Posts`).
|
||||||
|
- These classes wrap `getCollection` and `getEntry` from `astro:content`.
|
||||||
|
- They provide helper methods like `getPublished()`, sorting, and mapping.
|
||||||
|
- **Export**: A central `data` object aggregates all services.
|
||||||
|
|
||||||
|
**Example (`src/data/data.posts.ts`):**
|
||||||
|
```typescript
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
|
class Posts {
|
||||||
|
// Private mapper for transforming raw entries
|
||||||
|
#map = (post) => {
|
||||||
|
return { ...post, derivedProp: '...' };
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPublished = async () => {
|
||||||
|
const collection = await getCollection('posts');
|
||||||
|
return collection
|
||||||
|
.map(this.#map)
|
||||||
|
.sort((a, b) => b.data.pubDate - a.data.pubDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const posts = new Posts();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
import { data } from "~/data/data";
|
||||||
|
const posts = await data.posts.getPublished();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Collections (`src/content/`)
|
||||||
|
- Defined in `src/content.config.ts`.
|
||||||
|
- Uses `zod` for schema validation.
|
||||||
|
- Loaders: Uses `glob` loader.
|
||||||
|
- Current Collections: `posts`, `experiences`, `skills`.
|
||||||
|
|
||||||
|
## 5. Environment & Configuration
|
||||||
|
- **Package Manager**: `pnpm`
|
||||||
|
- **Config**: `astro.config.ts` handles integrations (MDX, Sitemap, Icon, etc.).
|
||||||
|
- **TS Config**: `tsconfig.json` extends `astro/tsconfigs/strict`.
|
||||||
|
|
||||||
|
## 6. Dependencies
|
||||||
|
- **Core**: `astro`, `@astrojs/mdx`, `astro-icon`.
|
||||||
|
- **Styling**: Standard CSS (scoped), `less` is in devDependencies.
|
||||||
|
- **Assets**: `@fontsource/vt323` (font), `sharp` (image processing).
|
||||||
@@ -21,6 +21,9 @@ const getSiteInfo = () => {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
...getSiteInfo(),
|
...getSiteInfo(),
|
||||||
output: 'static',
|
output: 'static',
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
integrations: [mdx(), sitemap(), icon(), compress({
|
integrations: [mdx(), sitemap(), icon(), compress({
|
||||||
HTML: false,
|
HTML: false,
|
||||||
}), robotsTxt()],
|
}), robotsTxt()],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"canvas": "^3.2.0",
|
"canvas": "^3.2.0",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.0",
|
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"vite-plugin-pwa": "^1.2.0"
|
"vite-plugin-pwa": "^1.2.0"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const links = {
|
|||||||
<style>
|
<style>
|
||||||
.header {
|
.header {
|
||||||
max-width: var(--content-width);
|
max-width: var(--content-width);
|
||||||
margin: 80px auto;
|
margin: 80px auto 0 auto;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { profile } from "./data.profile";
|
|||||||
|
|
||||||
class Posts {
|
class Posts {
|
||||||
#map = (post: CollectionEntry<'posts'>) => {
|
#map = (post: CollectionEntry<'posts'>) => {
|
||||||
|
const readingTime = Math.ceil(post.body?.split(/\s+/g).length / 200) || 1;
|
||||||
return Object.assign(post, {
|
return Object.assign(post, {
|
||||||
|
readingTime,
|
||||||
jsonLd: {
|
jsonLd: {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'BlogPosting',
|
'@type': 'BlogPosting',
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ const jsonLd = await profile.getJsonLd();
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a class="title" href={`/posts/${post.id}`}><h3>{post.data.title}</h3></a>
|
<a class="title" href={`/posts/${post.id}`}><h3>{post.data.title}</h3></a>
|
||||||
<div class="subtitle"><AbsoluteTime datetime={post.data.pubDate} /></div>
|
<div class="subtitle">
|
||||||
|
<AbsoluteTime
|
||||||
|
datetime={post.data.pubDate}
|
||||||
|
format={{ month: "long", day: "numeric", year: "numeric" }}
|
||||||
|
/>
|
||||||
|
{" "}• {post.readingTime} min read
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +54,7 @@ const jsonLd = await profile.getJsonLd();
|
|||||||
.main {
|
.main {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
max-width: var(--content-width);
|
max-width: var(--content-width);
|
||||||
margin: auto;
|
margin: 80px auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: calc(var(--gap) * 3);
|
gap: calc(var(--gap) * 3);
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const { Content } = await render(post);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
max-width: 100vw;
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
padding: var(--space-xl);
|
padding: var(--space-xl);
|
||||||
padding-left: var(--left-padding);
|
padding-left: var(--left-padding);
|
||||||
@@ -209,8 +210,11 @@ const { Content } = await render(post);
|
|||||||
padding-bottom: var(--space-md);
|
padding-bottom: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
pre {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
padding: var(--space-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +230,7 @@ const { Content } = await render(post);
|
|||||||
|
|
||||||
article picture {
|
article picture {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: -1;
|
z-index: 0;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +243,7 @@ const { Content } = await render(post);
|
|||||||
header {
|
header {
|
||||||
padding: var(--space-xl);
|
padding: var(--space-xl);
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user