diff --git a/astro.config.mjs b/astro.config.mjs index 98573e0..d0e74c6 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,18 +1,52 @@ import sitemap from '@astrojs/sitemap'; import { defineConfig } from 'astro/config'; +import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import rehypeSlug from 'rehype-slug'; export default defineConfig({ site: 'https://schmelczer.dev', trailingSlash: 'always', + redirects: { + '/writing/': '/articles/', + '/writing/[slug]': '/articles/[slug]', + }, integrations: [ sitemap({ - filter: (page) => !new URL(page).pathname.startsWith('/writing/'), + filter: (page) => { + const path = new URL(page).pathname; + return !path.startsWith('/writing/') && path !== '/404/'; + }, + serialize(item) { + return { ...item, changefreq: 'monthly' }; + }, }), ], + image: { + service: { entrypoint: 'astro/assets/services/sharp' }, + }, markdown: { shikiConfig: { - theme: 'github-light', + themes: { + light: 'github-light', + dark: 'github-dark', + }, + defaultColor: false, wrap: false, }, + rehypePlugins: [ + rehypeSlug, + [ + rehypeAutolinkHeadings, + { + behavior: 'append', + properties: { + className: ['heading-anchor'], + 'aria-hidden': 'true', + tabIndex: -1, + }, + content: { type: 'text', value: '#' }, + }, + ], + ], }, }); diff --git a/package-lock.json b/package-lock.json index 88a4c7e..844c1ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "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", "sharp": "^0.32.6", "typescript": "^5.9.3" } @@ -3586,6 +3588,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -3684,6 +3700,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", @@ -5399,6 +5429,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-parse": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", @@ -5431,6 +5480,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-stringify": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", @@ -9189,6 +9256,15 @@ "web-namespaces": "^2.0.0" } }, + "hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0" + } + }, "hast-util-is-element": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", @@ -9262,6 +9338,15 @@ "zwitch": "^2.0.0" } }, + "hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0" + } + }, "hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", @@ -10374,6 +10459,20 @@ "unified": "^11.0.0" } }, + "rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + } + }, "rehype-parse": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", @@ -10396,6 +10495,19 @@ "vfile": "^6.0.0" } }, + "rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "requires": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + } + }, "rehype-stringify": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", diff --git a/package.json b/package.json index a19cd01..261c11d 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "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", "sharp": "^0.32.6", "typescript": "^5.9.3" } diff --git a/public/og-image.jpg b/public/og-image.jpg index a5f6a31..248193d 100644 Binary files a/public/og-image.jpg and b/public/og-image.jpg differ diff --git a/public/robots.txt b/public/robots.txt index c2a49f4..76f21d4 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,4 @@ User-agent: * Allow: / + +Sitemap: https://schmelczer.dev/sitemap-index.xml diff --git a/public/site.webmanifest b/public/site.webmanifest index 32e25bf..4a2a981 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,11 +1,23 @@ { - "name": "Portfolio - Andras Schmelczer", - "short_name": "Portfolio", + "name": "Andras Schmelczer", + "short_name": "Schmelczer", + "description": "Andras Schmelczer writes about software systems, AI deployment, graphics, simulations, and tools.", + "lang": "en", + "id": "/", + "categories": ["education", "personal", "technology"], "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } + { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } ], - "theme_color": "#B7455E", - "background_color": "#242638", - "display": "standalone" + "theme_color": "#fbfaf7", + "background_color": "#fbfaf7", + "display": "standalone", + "start_url": "/", + "scope": "/" } diff --git a/scripts/check-overflow.mjs b/scripts/check-overflow.mjs index b60922b..bf93618 100644 --- a/scripts/check-overflow.mjs +++ b/scripts/check-overflow.mjs @@ -1,19 +1,10 @@ import { createServer } from 'node:http'; -import { readFile, stat } from 'node:fs/promises'; +import { readdir, readFile, stat } from 'node:fs/promises'; import path from 'node:path'; import { chromium } from 'playwright'; const dist = path.resolve('dist'); -const routes = [ - '/', - '/articles/', - '/articles/greatai-ai-deployment-api/', - '/writing/', - '/writing/greatai-ai-deployment-api/', - '/projects/', - '/about/', -]; -const widths = [320, 390, 430]; +const widths = [320, 390, 430, 768, 1024, 1440, 1920]; function contentType(file) { if (file.endsWith('.html')) return 'text/html; charset=utf-8'; @@ -27,6 +18,38 @@ function contentType(file) { return 'application/octet-stream'; } +async function walk(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walk(fullPath))); + } else { + files.push(fullPath); + } + } + return files; +} + +async function discoverRoutes() { + const files = await walk(dist); + const routes = new Set(); + for (const file of files) { + if (!file.endsWith('.html')) continue; + const rel = path.relative(dist, file).replaceAll(path.sep, '/'); + if (rel === '404.html') continue; + if (rel.endsWith('/index.html')) { + routes.add('/' + rel.slice(0, -'index.html'.length)); + } else if (rel === 'index.html') { + routes.add('/'); + } else { + routes.add('/' + rel.replace(/\.html$/, '/')); + } + } + return [...routes].sort(); +} + async function resolveFile(url) { const parsed = new URL(url, 'http://localhost'); const safePath = path @@ -52,6 +75,8 @@ async function resolveFile(url) { return path.join(dist, '404.html'); } +const routes = await discoverRoutes(); + const server = createServer(async (req, res) => { try { const file = await resolveFile(req.url ?? '/'); @@ -122,4 +147,6 @@ if (failures.length > 0) { process.exit(1); } -console.log('No horizontal overflow detected at 320px, 390px, or 430px.'); +console.log( + `No horizontal overflow detected at ${widths.join(', ')}px across ${routes.length} routes.` +); diff --git a/src/assets/og-default.jpg b/src/assets/og-default.jpg new file mode 100644 index 0000000..248193d Binary files /dev/null and b/src/assets/og-default.jpg differ diff --git a/src/components/ArticleList.astro b/src/components/ArticleList.astro index e8530da..40a0a40 100644 --- a/src/components/ArticleList.astro +++ b/src/components/ArticleList.astro @@ -1,8 +1,8 @@ --- import type { CollectionEntry } from 'astro:content'; -import { Image } from 'astro:assets'; -import { articlePath, formatDate } from '../lib/site'; +import EntryThumbnail from './EntryThumbnail.astro'; import TagList from './TagList.astro'; +import { articlePath, formatDate, formatDateShort } from '../lib/site'; interface Props { posts: CollectionEntry<'posts'>[]; @@ -10,7 +10,7 @@ interface Props { currentTag?: string; } -const { posts, showYear = false, currentTag } = Astro.props; +const { posts, showYear = true, currentTag } = Astro.props; ---