Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions .github/workflows/build-deploy-site.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
name: "Build and Deploy site"
on: [push]

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
Build:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 16.x
- run: npm ci
- run: npm run build -- --prefix-paths
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run build
run: npm run build
env:
NODE_ENV: production

- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
cname: learning-architect.blog
109 changes: 109 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a technical blog called "Блог обучающегося архитектора" (Learning Architect's Blog) hosted at https://learning-architect.blog. The blog focuses on software architecture, modern development practices, AI/LLM usage in development, DevOps, and technical design patterns.

## Tech Stack

- **Gatsby v5.14.5** - Static site generator
- **React v18.2.0** - UI framework
- **MDX v2.3.0** - Markdown with JSX for content authoring
- **TypeScript** - For component development (.tsx files)
- **Tailwind CSS v3.4.15** - Utility-first CSS framework
- **Sass** - CSS preprocessor
- **PrismJS** - Code syntax highlighting

## Development Commands

```bash
npm run develop # Start development server at localhost:8000
npm run build # Build production-ready static site
npm run serve # Serve production build locally
npm run clean # Clean Gatsby cache and public folders
```

## Content Structure

Blog posts are written in MDX format and stored in `/src/pages/`. Each article must include frontmatter with:

```mdx
---
slug: "your-article-slug"
date: "2024-12-10"
author: "Evgeniy Moroz"
keywords:
- keyword1
- keyword2
---
```

Article excerpts are defined using `{/* cut */}` JSX comments in the MDX content. The excerpt system works as follows:

- **Two `{/* cut */}` markers**: Content between the first and second `{/* cut */}` becomes the excerpt
- **One `{/* cut */}` marker**: Content before the first `{/* cut */}` becomes the excerpt
- **No markers**: Falls back to Gatsby's default excerpt

The excerpt is automatically converted from Markdown to HTML and displayed in the article summary section on the homepage.

## Architecture

### Key Components
- `src/components/WithSidebarLayout.tsx` - Main layout wrapper
- `src/components/DefaultPageLayout.tsx` - Layout for MDX pages
- `src/pages/index.tsx` - Homepage listing all blog posts

### Data Flow
- GraphQL queries fetch blog post data
- Static generation at build time
- RSS feed automatically generated at `/rss.xml`
- Sitemap generated at `/sitemap.xml`

### Styling
- Tailwind CSS v3 for utility classes with JIT mode enabled
- Global styles in `src/styles/global.css` (imported in gatsby-browser.js)
- Component styles use Tailwind utilities
- Code blocks styled with Prism twilight theme
- Custom CSS classes defined: `headline`, `secondary-h`, `sub-h`, `article`, `link-default`

## Creating New Blog Posts

1. Create a new `.mdx` file in `/src/pages/`
2. Add required frontmatter (slug, date, author, keywords)
3. Write content using Markdown and JSX components
4. Structure content with `{/* cut */}` markers for excerpts:
```mdx
# Article Title
{/* cut */}
This is the excerpt content that will appear on the homepage.
{/* cut */}
Rest of the article content...
```
5. Run `npm run develop` to preview
6. Build with `npm run build` before deploying

## Important Configurations

- `gatsby-config.js` - Main Gatsby configuration
- `tailwind.config.js` - Tailwind CSS configuration
- `postcss.config.js` - PostCSS configuration for Tailwind processing
- `gatsby-browser.js` - Browser-specific imports (CSS and Prism theme)
- `gatsby-ssr.js` - Server-side rendering setup with critical CSS to prevent FOUC
- `gatsby-node.js` - Custom excerpt extraction logic for `{/* cut */}` tags
- Site metadata and RSS feed settings in gatsby-config.js

## Deployment

The site uses GitHub Actions for automated deployment:
- **Workflow**: `.github/workflows/build-deploy-site.yaml`
- **Node.js version**: 20.x LTS
- **Triggers**: Push to main branch, pull requests
- **Deployment**: Automatically deploys to GitHub Pages at learning-architect.blog
- **Build command**: `npm run build` (production optimized)

### GitHub Pages Setup Required:
1. Enable GitHub Pages in repository settings
2. Set source to "GitHub Actions"
3. Ensure `GITHUB_TOKEN` has write permissions for Pages
14 changes: 12 additions & 2 deletions gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import './src/styles/global.css';
import 'prismjs/themes/prism-twilight.css';
import React from "react"
import DefaultPageLayout from "./src/components/DefaultPageLayout"
import "./src/styles/global.css"
import "prismjs/themes/prism-twilight.css"

// Wrap all MDX pages with DefaultPageLayout
export const wrapPageElement = ({ element, props }) => {
// Check if this is an MDX page
if (props.pageContext && props.pageContext.frontmatter) {
return <DefaultPageLayout {...props}>{element}</DefaultPageLayout>
}
return element
}
40 changes: 26 additions & 14 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ module.exports = {
siteUrl: `https://learning-architect.blog`,
},
plugins: [
{
resolve: "gatsby-plugin-postcss",
options: {
cssLoaderOptions: {
modules: false,
},
},
},
"gatsby-plugin-sass",
"gatsby-plugin-postcss",
"gatsby-plugin-image",
"gatsby-plugin-react-helmet",
{
Expand All @@ -18,6 +25,13 @@ module.exports = {
{
resolve: "gatsby-plugin-mdx",
options: {
extensions: [`.mdx`, `.md`],
mdxOptions: {
remarkPlugins: [
require('remark-gfm'),
],
rehypePlugins: [],
},
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
Expand All @@ -31,9 +45,6 @@ module.exports = {
},
},
],
defaultLayouts: {
default: require.resolve(`./src/components/DefaultPageLayout.tsx`),
},
}
},
`gatsby-remark-images`,
Expand Down Expand Up @@ -74,32 +85,33 @@ module.exports = {
{
serialize: ({query: {site, allMdx}}) => {
return allMdx.nodes.map(node => {
const slug = node.internal.contentFilePath.replace(/^.*\/src\/pages\//, '').replace(/\.mdx?$/, '')
const title = slug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
return Object.assign({}, node.frontmatter, {
description: node.fields.articleCut,
description: node.fields ? node.fields.articleCut : node.excerpt,
date: node.frontmatter.date,
title: node.headings[0].value,
url: `${site.siteMetadata.siteUrl}/${node.slug}/`,
guid: `${site.siteMetadata.siteUrl}/${node.slug}/`
title: title,
url: `${site.siteMetadata.siteUrl}/${slug}/`,
guid: `${site.siteMetadata.siteUrl}/${slug}/`
})
})
},
query: `
{
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
sort: { frontmatter: { date: DESC } }
) {
nodes {
id
headings(depth: h1) {
value
}
excerpt
internal {
contentFilePath
}
fields {
articleCut
}
slug
frontmatter {
date
title
author
keywords
}
Expand Down
74 changes: 62 additions & 12 deletions gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,72 @@
const remark = require('remark')
const remarkHTML = require('remark-html')
const fs = require('fs')

const EXCERPT_SEPARATOR = '<!-- cut -->'
const EXCERPT_SEPARATOR = '{/* cut */}'

// Simple markdown to HTML converter
function markdownToHtml(markdown) {
return markdown
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
.replace(/~~(.*?)~~/gim, '<del>$1</del>')
.replace(/\[(.*?)\]\((.*?)\)/gim, '<a href="$2">$1</a>')
.replace(/`(.*?)`/gim, '<code>$1</code>')
.replace(/\n\n+/gim, '</p><p>')
.replace(/^(.+)$/gim, '<p>$1</p>')
.replace(/<p><\/p>/gim, '')
.replace(/<p>(<h[1-6]>.*<\/h[1-6]>)<\/p>/gim, '$1')
.replace(/<p>(<ul>.*<\/ul>)<\/p>/gims, '$1')
.replace(/<p>(<ol>.*<\/ol>)<\/p>/gims, '$1');
}

exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions

if (node.internal.type === `Mdx`) {
const cutBeginning = node.rawBody.indexOf(EXCERPT_SEPARATOR) + EXCERPT_SEPARATOR.length;
const cutEnding = node.rawBody.lastIndexOf(EXCERPT_SEPARATOR);
if (cutEnding === cutBeginning) {
throw new Error('Please put 2 <!-- cut --> tags in the article. At the beginning and at the end.')
let articleCut = '';

try {
// Get the raw file content directly
const filePath = node.fileAbsolutePath || node.internal.contentFilePath;
if (filePath && fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');

// Find the position of excerpt separators
const firstCutIndex = content.indexOf(EXCERPT_SEPARATOR);
const secondCutIndex = content.indexOf(EXCERPT_SEPARATOR, firstCutIndex + 1);

if (firstCutIndex !== -1) {
let excerptContent = '';

if (secondCutIndex !== -1) {
// Content between first and second cut
excerptContent = content.substring(firstCutIndex + EXCERPT_SEPARATOR.length, secondCutIndex).trim();
} else {
// Content before first cut (fallback)
excerptContent = content.substring(0, firstCutIndex).trim();
// Remove frontmatter if present
excerptContent = excerptContent.replace(/^---[\s\S]*?---\s*/m, '');
}

if (excerptContent) {
// Convert to HTML
articleCut = markdownToHtml(excerptContent);
}
} else {
// Fallback to default excerpt if no separator found
articleCut = node.excerpt || '';
}
} else {
// Fallback to default excerpt
articleCut = node.excerpt || '';
}
} catch (error) {
console.warn('Error processing excerpt for', node.internal.contentFilePath, error);
articleCut = node.excerpt || '';
}

const rawExcerpt = node.rawBody.substr(cutBeginning, cutEnding - cutBeginning)
const articleCut = rawExcerpt
? remark().use(remarkHTML).processSync(rawExcerpt.trim()).toString()
: ''

createNodeField({
name: `articleCut`,
node,
Expand Down
37 changes: 37 additions & 0 deletions gatsby-ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react"
import DefaultPageLayout from "./src/components/DefaultPageLayout"
import "./src/styles/global.css"
import "prismjs/themes/prism-twilight.css"

// Wrap all MDX pages with DefaultPageLayout (same as gatsby-browser.js)
export const wrapPageElement = ({ element, props }) => {
// Check if this is an MDX page
if (props.pageContext && props.pageContext.frontmatter) {
return <DefaultPageLayout {...props}>{element}</DefaultPageLayout>
}
return element
}

// Add preload hints for critical CSS
export const onRenderBody = ({ setHeadComponents }) => {
setHeadComponents([
<style
key="critical-css"
dangerouslySetInnerHTML={{
__html: `
/* Critical CSS to prevent FOUC */
body { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; }
.container { width: 100%; max-width: 80rem; margin: 0 auto; }
.grid { display: grid; }
.bg-white { background-color: white; }
.text-gray-500 { color: rgb(107, 114, 128); }
.text-gray-700 { color: rgb(55, 65, 81); }
.text-yellow-600 { color: rgb(202, 138, 4); }
.headline { font-size: 1.875rem; line-height: 2.25rem; color: rgb(17, 24, 39); }
.secondary-h { font-size: 1.5rem; line-height: 2rem; color: rgb(202, 138, 4); }
.sub-h { font-size: 1.25rem; line-height: 1.25rem; color: rgb(107, 114, 128); }
`,
}}
/>,
])
}
Loading