diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..44788baf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,154 @@ +## AGENTS + +Purpose: Help AI coding agents work productively in this Gatsby (v5) + MDX (v2) blog. + +Big picture +- Static site built with Gatsby 5.x, content in `content/blog/**/index.md` (MDX). Images live next to posts. +- Programmatic pages are created in `gatsby-node.ts` using GraphQL over MDX nodes; custom fields `fields.slug` and `fields.postPath` are added in `onCreateNode` and used across templates. +- Main templates live in `src/templates/` (blog-post, blog-page, tag-page, author-page). Shared UI in `src/components/`. +- Site config and plugin wiring in `gatsby-config.js`. Two filesystem sources (`content/blog`, `content/assets`). MDX is configured with remark plugins (images, prism, etc.). + # AGENTS + +Purpose: Help AI coding agents work productively in this Gatsby (v5) + MDX (v2) blog. + +Big picture + +- Static site built with Gatsby 5.x, content in `content/blog/**/index.md` (MDX). Images live next to posts. + +- Programmatic pages are created in `gatsby-node.ts` using GraphQL over MDX nodes; custom fields `fields.slug` and `fields.postPath` are added in `onCreateNode` and used across templates. + +- Main templates live in `src/templates/` (blog-post, blog-page, tag-page, author-page). Shared UI in `src/components/`. + +- Site config and plugin wiring in `gatsby-config.js`. Two filesystem sources (`content/blog`, `content/assets`). MDX is configured with remark plugins (images, prism, etc.). +# AGENTS + +Purpose: Help AI coding agents work productively in this Gatsby (v5) + MDX (v2) blog. + +Big picture + +- Static site built with Gatsby 5.x, content in `content/blog/**/index.md` (MDX). Images live next to posts. + +- Programmatic pages are created in `gatsby-node.ts` using GraphQL over MDX nodes; custom fields `fields.slug` and `fields.postPath` are added in `onCreateNode` and used across templates. + +- Main templates live in `src/templates/` (blog-post, blog-page, tag-page, author-page). Shared UI in `src/components/`. + +- Site config and plugin wiring in `gatsby-config.js`. Two filesystem sources (`content/blog`, `content/assets`). MDX is configured with remark plugins (images, prism, etc.). + +Key workflows + +- Install deps: `npm ci` (use Node 18+). + +- Dev server: `npm run develop` (hot-reload preview while editing content/components). + +- Build: `npm run build` then preview: `npm run serve` (site at ). + +- Test script: `npm run test` runs lint then build. Lint uses ESLint 8 with `.eslintrc` (flat config is NOT used here). + +Project-specific conventions + +- Posts are MDX files at `content/blog//index.md` with frontmatter: `title`, `date` (ISO8601), `author`, optional `featuredImage`, `description` for home excerpts. See README for examples. + +- Author metadata in `content/authors.json`; author images resolved to `content/assets/authors/.jpg`. If missing, `gatsby-node.ts` downloads GitHub avatar at build time using global `fetch` and writes to that path. + +- GraphQL usage relies on custom `fields` on `Mdx` nodes (set in `gatsby-node.ts`). Templates query `fields { slug, postPath }` and `frontmatter { ... }`. When creating new templates/queries, ensure those fields exist or add similar fields in `onCreateNode`. + +- Pagination: `gatsby-node.ts` creates `/page/1..N`; index shows up to 10 posts, template queries use `$skip`/`$limit`. + +- RSS: only generated in production (`npm run build && npm run serve`). + +Important files + +- `gatsby-node.ts`: page creation, tag pages, author pages, MDX fields. Uses Node 18 global `fetch` and `fs.promises.writeFile(new Uint8Array(...))` when downloading author avatars. + +- `gatsby-config.js`: plugins & site metadata; MDX/remark configuration; Google Analytics; feed; typography. + +- `src/templates/*.js`: GraphQL queries + rendering logic per page type. + +- `src/components/*`: Layout, SEO, ArticlePreview, Byline, Tags, Comments, etc. + +- `content/`: blog posts and assets; authors.json. + +Patterns to follow + +- When adding GraphQL fields to `Mdx`, define them in `onCreateNode` (slug, postPath). All templates assume these fields are present. + +- Use `internal.contentFilePath` when creating pages for MDX (`?__contentFilePath=`) to enable MDX rendering. + +- For author pages, ensure each author key in `authors.json` maps to a `.jpg` in `content/assets/authors/` or the avatar fetch will run at build time. + +- Keep queries in templates in sync with the schema defined by the plugins and the fields created in `gatsby-node.ts`. + +Common pitfalls + +- If you see GraphQL errors like “Cannot query field 'fields' on type 'Mdx'”, check `onCreateNode` in `gatsby-node.ts` is creating those fields and that Gatsby is picking up the TS node file (it is, via `gatsby-node.ts`). Run `npm run clean` before `npm run build` after changes. + +- Do not upgrade React to v19 or MDX to v3 without coordinating upgrades to Gatsby v6+ and `gatsby-plugin-mdx` v6+. This repo is pinned to Gatsby 5.x with MDX v2. + +- ESLint 8 with `.eslintrc` is the current setup; ESLint 9 flat config caused parsing issues. Update only if necessary, and migrate carefully. + +Examples + +- Creating a new blog post: add `content/blog/hello-world/index.md` with frontmatter, optional images; run `npm run develop`, verify at `/` and `/page/2`. + +- Linking to posts: use `node.fields.postPath` provided by `gatsby-node.ts` when building lists (see `src/templates/blog-page.js`). + +Local commands (copy/paste) + +- Install: `npm ci` + +- Dev: `npm run develop` + +- Clean + build: `npm run clean && npm run build` + +- Preview build: `npm run serve` () + +Maintainer notes for agents + +- Prefer minimal, surgical changes. Avoid upgrading major versions unless explicitly requested. + +- When changing GraphQL, run a clean build to regenerate `.cache/schema.gql` and catch schema issues early. + +Azure note + +- If asked to generate Azure code or commands, follow the Azure best-practices tool guidance available in your environment. + +Contribution flow + +- Create feature branches from `master` (or the working update branch if collaborating). + +- Validate locally before PR: + + - `npm ci` + + - `npm run test` (runs lint + build) + + - Optional: `npm run serve` and verify key pages (/, /page/2, a post, /tags/{tag}, /author/{key}). + +- Keep changes minimal; avoid dependency major bumps unless the PR is dedicated to that effort. + +- In PRs, call out changes to GraphQL queries/templates and any content structure updates. +Maintainer notes for agents + +- Prefer minimal, surgical changes. Avoid upgrading major versions unless explicitly requested. + +- When changing GraphQL, run a clean build to regenerate `.cache/schema.gql` and catch schema issues early. + +Azure note + +- If asked to generate Azure code or commands, follow the Azure best-practices tool guidance available in your environment. + +Contribution flow + +- Create feature branches from `master` (or the working update branch if collaborating). + +- Validate locally before PR: + + - `npm ci` + + - `npm run test` (runs lint + build) + + - Optional: `npm run serve` and verify key pages (/, /page/2, a post, /tags/, /author/). + +- Keep changes minimal; avoid dependency major bumps unless the PR is dedicated to that effort. + +- In PRs, call out changes to GraphQL queries/templates and any content structure updates. diff --git a/README.md b/README.md index c7e9459ed..670b58212 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Our CSS will take care of rendering it correctly by looking for `img + em`. Example: - ![Photo depiciting a drop of water](./clean-drop-of-water-liquid.jpg) + ![Photo depicting a drop of water](./clean-drop-of-water-liquid.jpg) *AQA v1.0 is a first drop in an on-going series of improvements.* ### Quotes diff --git a/SECURITY.md b/SECURITY.md index c252bf5b9..230665147 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,6 @@ # Security Policy + ## Reporting a Vulnerability + Please report vulnerabilities to [security@adoptopenjdk.net](security@adoptopenjdk.net). A member of the security team will respond within 48 hours with details on how to proceed including whether or not the vulnerability was accepted, declined or requires further investigation. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..0d08d47b5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,26 @@ +import babelParser from '@babel/eslint-parser'; +import reactPlugin from 'eslint-plugin-react'; + +export default [ + { + ignores: ['**/public/**', '**/node_modules/**'], + }, + { + files: ['**/*.js', '**/*.jsx'], + languageOptions: { + parser: babelParser, + parserOptions: { + requireConfigFile: false, + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + globals: { window: 'readonly', document: 'readonly' }, + }, + plugins: { react: reactPlugin }, + rules: { + // basic recommended ruleset + }, + }, +]; + diff --git a/gatsby-node.js b/gatsby-node.js new file mode 100644 index 000000000..099f2d732 --- /dev/null +++ b/gatsby-node.js @@ -0,0 +1,144 @@ +const path = require('path'); +const fs = require('fs'); +const { createFilePath } = require('gatsby-source-filesystem'); + +exports.createPages = async ({ graphql, actions }) => { + const { createPage } = actions; + + const authorJson = require('./content/authors.json'); + const authorPage = path.resolve('./src/templates/author-page.js'); + + for (let author of Object.keys(authorJson)) { + try { + await fs.promises.access(`content/assets/authors/${author}.jpg`); + } catch (error) { + const githubUsername = authorJson[author].github; + const response = await fetch(`https://github.com/${githubUsername}.png?size=250`); + if (!response.ok) { + throw new Error(`Unexpected response: ${response.statusText}`); + } + const arrayBuf = await response.arrayBuffer(); + const uint8 = new Uint8Array(arrayBuf); + await fs.promises.writeFile(`content/assets/authors/${author}.jpg`, uint8); + } + + createPage({ + path: `/author/${author}`, + component: authorPage, + context: { + author: author, + limit: 10, + }, + }); + } + + const tagTemplate = path.resolve('./src/templates/tag-page.js'); + const blogPost = path.resolve('./src/templates/blog-post.js'); + const result = await graphql(` + { + allMdx(sort: {frontmatter: {date: DESC}}) { + edges { + node { + fields { + slug + postPath + } + frontmatter { + title + tags + } + internal { + contentFilePath + } + } + } + } + tagsGroup: allMdx(limit: 2000) { + group(field: {frontmatter: {tags: SELECT}}) { + fieldValue + } + } + } + `); + + if (result.errors) { + throw result.errors; + } + + if (!result.data) { + throw new Error('Error retrieving blog posts'); + } + + const posts = result.data.allMdx.edges; + + posts.forEach((post, index) => { + const previous = index === posts.length - 1 ? null : posts[index + 1].node; + const next = index === 0 ? null : posts[index - 1].node; + + createPage({ + path: `${post.node.fields.postPath}`, + component: `${blogPost}?__contentFilePath=${post.node.internal.contentFilePath}`, + context: { + slug: post.node.fields.slug, + postPath: post.node.fields.postPath, + previous, + next, + }, + }); + }); + + const tags = result.data.tagsGroup.group; + + tags.forEach((tag) => { + createPage({ + path: `/tags/${tag.fieldValue}/`, + component: tagTemplate, + context: { + tag: tag.fieldValue, + }, + }); + }); + + const postsPerPage = 10; + const numPages = Math.ceil(posts.length / postsPerPage); + Array.from({ length: numPages }).forEach((_, index) => { + const currentPageNumber = index + 1; + const previousPageNumber = currentPageNumber === 1 ? null : currentPageNumber - 1; + const nextPageNumber = currentPageNumber === numPages ? null : currentPageNumber + 1; + + createPage({ + path: `/page/${index + 1}`, + component: path.resolve('./src/templates/blog-page.js'), + context: { + limit: postsPerPage, + skip: index * postsPerPage, + numPages, + currentPageNumber, + previousPageNumber, + nextPageNumber, + }, + }); + }); +}; + +exports.onCreateNode = async ({ node, actions, getNode }) => { + const { createNodeField } = actions; + + if (node.internal.type === 'Mdx') { + const slug = createFilePath({ node, getNode }); + const date = new Date(node.frontmatter.date); + const year = date.getFullYear(); + const zeroPaddedMonth = `${date.getMonth() + 1}`.padStart(2, '0'); + + createNodeField({ + name: 'slug', + node, + value: slug, + }); + createNodeField({ + name: 'postPath', + node, + value: `/${year}/${zeroPaddedMonth}${slug}`, + }); + } +};