Switch the site to Astro
This commit is contained in:
parent
a5f64a3ff8
commit
2e02e52661
14 changed files with 8633 additions and 17018 deletions
|
|
@ -1 +0,0 @@
|
|||
**/*.js
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["unused-imports", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports-ts": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
}
|
||||
}
|
||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
|
@ -1,11 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
target
|
||||
.astro
|
||||
.DS_Store
|
||||
|
|
|
|||
12
.prettierrc
12
.prettierrc
|
|
@ -4,7 +4,13 @@
|
|||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf",
|
||||
"importOrder": ["^[./]", ".*", ".scss$"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -37,6 +37,6 @@
|
|||
"files.exclude": {
|
||||
"node_modules": true
|
||||
},
|
||||
"editor.rulers": [120],
|
||||
"editor.rulers": [90],
|
||||
"editor.wordWrap": "on"
|
||||
}
|
||||
|
|
|
|||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
|
|
@ -2,7 +2,7 @@
|
|||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Format and lint",
|
||||
"label": "Lint",
|
||||
"type": "shell",
|
||||
"command": "npm run lint",
|
||||
"group": "test",
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -1,21 +1,35 @@
|
|||
# Portfolio
|
||||
# schmelczer.dev
|
||||
|
||||
> An easy-to-configure timeline for your projects.
|
||||
A static personal blog for Andras Schmelczer, built with Astro.
|
||||
|
||||
[Check out the live version.](https://schmelczer.dev)
|
||||
The site is article-first: articles live in `src/content/posts`, project index entries
|
||||
live in `src/content/projects`, and normal pages are rendered as static HTML with no
|
||||
required client JavaScript.
|
||||
|
||||
## Configuration
|
||||
## Setup
|
||||
|
||||
- The actual content is in the [data](src/data) folder, starting with [portfolio.ts](src/data/portfolio.ts)
|
||||
- The assets referenced should be located in [data/media](src/data/media)
|
||||
```sh
|
||||
npm ci
|
||||
npx playwright install --with-deps chromium # required before `npm run qa:overflow`
|
||||
```
|
||||
|
||||
## Build
|
||||
## Commands
|
||||
|
||||
1. `npm install`
|
||||
2. `npm run build`
|
||||
3. You can find the results in the [dist](dist) folder
|
||||
```sh
|
||||
npm run dev
|
||||
npm run lint
|
||||
npm run build
|
||||
npm run preview
|
||||
npm run qa
|
||||
```
|
||||
|
||||
## Info
|
||||
## Structure
|
||||
|
||||
- All images are converted to `WebP` after being imported into any file.
|
||||
> Except for the og-image, and SVGs.
|
||||
- `src/content/posts`: Markdown articles
|
||||
- `src/content/projects`: project index entries
|
||||
- `src/pages`: static routes
|
||||
- `src/layouts`: page and post layouts
|
||||
- `src/components`: reusable UI pieces
|
||||
- `src/styles/global.css`: the visual system
|
||||
- `public/media/downloads`: CV and thesis PDFs
|
||||
- `public/media/video`: project videos
|
||||
|
|
|
|||
136
astro.config.mjs
Normal file
136
astro.config.mjs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
// Build a lookup of post slugs to their last modification dates so the sitemap
|
||||
// can advertise accurate <lastmod> values to crawlers. astro:content isn't
|
||||
// available inside the config, so we read post frontmatter directly. Our posts
|
||||
// always use single-line scalar `date:` / `updated:` keys, so a small regex
|
||||
// extraction is sufficient and intentional.
|
||||
const postsDir = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'src/content/posts'
|
||||
);
|
||||
|
||||
function extractScalar(frontmatter, key) {
|
||||
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm'));
|
||||
return match?.[1]?.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
const postLastmodLookup = new Map(
|
||||
readdirSync(postsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map((entry) => {
|
||||
const raw = readFileSync(path.join(postsDir, entry.name), 'utf8');
|
||||
const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
|
||||
const rawDate =
|
||||
extractScalar(frontmatter, 'updated') ?? extractScalar(frontmatter, 'date');
|
||||
const parsed = rawDate ? new Date(rawDate) : null;
|
||||
const valid = parsed && !Number.isNaN(parsed.valueOf()) ? parsed : null;
|
||||
return [entry.name.replace(/\.md$/, ''), valid];
|
||||
})
|
||||
.filter(([, date]) => date !== null)
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://schmelczer.dev',
|
||||
trailingSlash: 'ignore',
|
||||
integrations: [
|
||||
sitemap({
|
||||
filter: (page) => {
|
||||
const path = new URL(page).pathname;
|
||||
return !/^\/tags\/[^/]+\/?$/.test(path) && path !== '/404/';
|
||||
},
|
||||
serialize(item) {
|
||||
const url = new URL(item.url);
|
||||
const match = url.pathname.match(/^\/articles\/([^/]+)\/?$/);
|
||||
let lastmod = item.lastmod;
|
||||
if (match) {
|
||||
const date = postLastmodLookup.get(match[1]);
|
||||
if (date instanceof Date && !Number.isNaN(date.valueOf())) {
|
||||
lastmod = date.toISOString();
|
||||
}
|
||||
}
|
||||
return { ...item, changefreq: 'monthly', ...(lastmod ? { lastmod } : {}) };
|
||||
},
|
||||
}),
|
||||
],
|
||||
image: {
|
||||
service: { entrypoint: 'astro/assets/services/sharp' },
|
||||
},
|
||||
vite: {
|
||||
server: {
|
||||
watch: {
|
||||
// Avoid inotify instance limits in dev containers and mounted volumes.
|
||||
usePolling: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark',
|
||||
},
|
||||
defaultColor: false,
|
||||
wrap: false,
|
||||
},
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
behavior: 'append',
|
||||
properties: {
|
||||
className: ['heading-anchor'],
|
||||
},
|
||||
// Glyph rendered via CSS ::before so it doesn't leak into the TOC
|
||||
// when astro:content extracts heading.text from the rendered HTML.
|
||||
content: [],
|
||||
},
|
||||
],
|
||||
// Make scrollable code blocks and tables reachable via keyboard (WCAG
|
||||
// 2.1.1): without tabindex, a keyboard user cannot focus a horizontally
|
||||
// overflowing <pre> or <table> to scroll it. tabindex=0 is sufficient
|
||||
// on its own; role=region would require a meaningful per-block label,
|
||||
// which we don't have at markdown level.
|
||||
function rehypeFocusableScrollables() {
|
||||
const SCROLLABLE = new Set(['pre', 'table']);
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node) => {
|
||||
if (!SCROLLABLE.has(node.tagName)) return;
|
||||
node.properties.tabindex = '0';
|
||||
});
|
||||
};
|
||||
},
|
||||
function rehypeLabelHeadingPermalinks() {
|
||||
function textOf(node) {
|
||||
if (!node) return '';
|
||||
if (node.type === 'text') return node.value ?? '';
|
||||
return (node.children ?? []).map(textOf).join('');
|
||||
}
|
||||
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node) => {
|
||||
if (!/^h[2-6]$/.test(node.tagName)) return;
|
||||
const headingText = textOf(node).trim();
|
||||
if (!headingText) return;
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
const className = child.properties?.className;
|
||||
const classes = Array.isArray(className) ? className : [className];
|
||||
if (child.tagName === 'a' && classes.includes('heading-anchor')) {
|
||||
child.properties.ariaLabel = `Permalink to ${headingText}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
39
custom.d.ts
vendored
39
custom.d.ts
vendored
|
|
@ -1,39 +0,0 @@
|
|||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
import { ResponsiveImage } from 'src/types/responsive-image';
|
||||
const content: ResponsiveImage;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
import { ResponsiveImage } from 'src/types/responsive-image';
|
||||
const content: ResponsiveImage;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.mp4' {
|
||||
import { url } from 'src/types/url';
|
||||
const content: url;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.webm' {
|
||||
import { url } from 'src/types/url';
|
||||
const content: url;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.pdf' {
|
||||
import { url } from 'src/types/url';
|
||||
const content: url;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.html' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
25139
package-lock.json
generated
25139
package-lock.json
generated
File diff suppressed because it is too large
Load diff
78
package.json
78
package.json
|
|
@ -1,60 +1,56 @@
|
|||
{
|
||||
"name": "portfolio",
|
||||
"description": "An easily configurable timeline of projects.",
|
||||
"name": "schmelczer-dev",
|
||||
"description": "A static personal blog for Andras Schmelczer.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "npm@10.9.2",
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack serve --open --mode development",
|
||||
"lint": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.(ts|scss|json|html)\"",
|
||||
"build": "webpack --mode production",
|
||||
"update": "ncu"
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"typecheck": "astro check",
|
||||
"lint": "prettier --check \"astro.config.mjs\" \"src/**/*.{astro,ts,md,css}\" \"scripts/*.mjs\" \"*.md\" \"*.json\"",
|
||||
"format": "prettier --write \"astro.config.mjs\" \"src/**/*.{astro,ts,md,css}\" \"scripts/*.mjs\" \"*.md\" \"*.json\"",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"qa:links": "node scripts/check-links.mjs",
|
||||
"qa:no-js": "node scripts/check-no-js.mjs",
|
||||
"qa:overflow": "node scripts/install-playwright-deps.mjs && node scripts/check-overflow.mjs",
|
||||
"qa": "npm run typecheck && npm run lint && npm run build && npm run qa:links && npm run qa:no-js && npm run qa:overflow"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/schmelczer/schmelczer.github.io.git"
|
||||
},
|
||||
"keywords": [
|
||||
"CV",
|
||||
"curriculum",
|
||||
"vitae",
|
||||
"portfolio",
|
||||
"resumé"
|
||||
"blog",
|
||||
"software engineering",
|
||||
"computer science",
|
||||
"portfolio"
|
||||
],
|
||||
"author": "Andras Schmelczer",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/schmelczer/schmelczer.github.io/issues"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"homepage": "https://github.com/schmelczer/schmelczer.github.io#readme",
|
||||
"devDependencies": {
|
||||
"@plausible-analytics/tracker": "^0.4.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"inline-source-webpack-plugin": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"npm-check-updates": "^16.14.4",
|
||||
"prettier": "^3.0.3",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"responsive-loader": "^3.1.2",
|
||||
"sass": "^1.68.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"sharp": "^0.32.6",
|
||||
"sitemap-webpack-plugin": "^1.1.1",
|
||||
"string-replace-loader": "^3.1.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"@astrojs/rss": "^4.0.18",
|
||||
"@astrojs/sitemap": "^3.7.2",
|
||||
"astro": "^6.3.1",
|
||||
"playwright": "^1.59.1",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"overrides": {
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,6 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"dom"
|
||||
]
|
||||
"types": ["astro/client"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin');
|
||||
const SitemapPlugin = require('sitemap-webpack-plugin').default;
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const domain = 'schmelczer.dev';
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
devtool: argv.mode === 'development' ? 'inline-source-map' : false,
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
},
|
||||
devServer: {
|
||||
allowedHosts: 'all',
|
||||
},
|
||||
watchOptions: {
|
||||
ignored: '**/node_modules',
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
module: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
performance: {
|
||||
assetFilter: (f) => !/\.(webm|mp4|pdf)$/.test(f),
|
||||
maxEntrypointSize: 100000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
plugins: [
|
||||
new SitemapPlugin({
|
||||
base: `https://${domain}`,
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
priority: 1,
|
||||
changefreq: 'daily',
|
||||
},
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
}),
|
||||
new MiniCssExtractPlugin(),
|
||||
argv.mode === 'production'
|
||||
? new InlineSourceWebpackPlugin({
|
||||
compress: true,
|
||||
})
|
||||
: null,
|
||||
new (require('webpack').DefinePlugin)({
|
||||
__CURRENT_DATE__: Date.now(),
|
||||
}),
|
||||
].filter(Boolean),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(jpe?g|png)$/i,
|
||||
exclude: /no-change/i,
|
||||
loader: 'responsive-loader',
|
||||
options: {
|
||||
adapter: require('responsive-loader/sharp'),
|
||||
sizes: [200, 500, 900, 1400, 1920],
|
||||
placeholder: true,
|
||||
placeholderSize: 64,
|
||||
quality: 85,
|
||||
format: 'webp',
|
||||
progressive: true,
|
||||
name: '[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(webm|mp4|woff2?)$/i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[hash:8][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
use: 'svg-inline-loader',
|
||||
},
|
||||
{
|
||||
test: /\/no-change\//i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[name][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /.pdf$/i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'static/[name][ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'resolve-url-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true, // required by resolve-url-loader
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
argv.mode === 'production'
|
||||
? {
|
||||
// for removing whitespace (mainly from template strings) which are not part of comments
|
||||
loader: 'string-replace-loader',
|
||||
options: {
|
||||
search: /(?<!\/\/[^\n]*)(\\n|\s)+/gs,
|
||||
replace: ' ',
|
||||
},
|
||||
}
|
||||
: null,
|
||||
'ts-loader',
|
||||
].filter(Boolean),
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.ts',
|
||||
'.js', // required for development
|
||||
],
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '',
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue