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
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 3
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Verify

on:
pull_request:
push:
branches: [main]

permissions:
contents: read
statuses: write

concurrency:
group: verify-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Test and build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Lint, format, type-check, and test
run: yarn lint && yarn format:check && yarn typecheck && yarn test:coverage

- name: Build the production application
run: yarn build

- name: Verify build output
run: test -f .next/BUILD_ID

- name: Install Playwright browser
run: yarn playwright install --with-deps chromium

- name: Run browser and accessibility tests
run: yarn test:e2e

- name: Run Lighthouse CI
env:
LHCI_GITHUB_TOKEN: ${{ github.token }}
run: yarn lighthouse
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ typings/

# Next.js build output
.next
.lighthouseci
playwright-report
test-results

# Nuxt.js build / generate output
.nuxt
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ yarn dev

Open [http://localhost:3000](http://localhost:3000).

## Verification

```bash
yarn test
yarn test:coverage
yarn verify
```

`yarn verify` runs the unit/content checks and creates a production build. GitHub Actions runs the same checks for pull requests and pushes to `main`. In GitHub repository settings, require the **Verify / Test and build** status check before merging to `main`; Vercel's production deployment will then only occur after the verified pull request is merged.

## How to deploy

This site is deployed like a standard Next.js app on Vercel, but you can deploy anywhere that supports Node.js 24.
Expand Down
17 changes: 4 additions & 13 deletions app/_meta.global.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
export default {
index: {
type: 'page',
title: 'About'
},
resume: {
type: 'page',
title: 'Resume'
},
index: { type: 'page', title: 'Home' },
resume: { type: 'page', title: 'Resume' },
recommendations: { type: 'page', title: 'Recommendations' },
posts: {
type: 'page',
title: 'Posts',
title: 'Writing',
items: {
'shortlisting-remote-companies': 'Shortlisting Remote Companies',
'working-remotely-from-pakistan': 'Working Remotely from Pakistan',
'apply-for-remote-jobs': 'Applying for Remote Jobs',
'standout-in-your-job-application': 'Stand Out in Your Job Application',
'advises-from-my-mentors': 'Advice from My Mentors'
}
},
more: {
type: 'page',
title: 'More'
}
}
23 changes: 23 additions & 0 deletions app/agent-markdown/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { markdownForPath } from '../../lib/agent-markdown'
import { contentSignal } from '../../data/profile'

export const dynamic = 'force-dynamic'

export async function GET(request) {
const path =
request.headers.get('x-agent-markdown-path') ??
new URL(request.url).searchParams.get('path') ??
'/'
const markdown = await markdownForPath(path)
const headers = {
'Content-Signal': contentSignal,
'Content-Type': 'text/markdown; charset=utf-8',
Vary: 'Accept',
'X-Robots-Tag': 'noindex, follow'
}

if (markdown === null)
return new Response('Not Found\n', { status: 404, headers })

return new Response(markdown, { headers })
}
Binary file added app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 18 additions & 31 deletions app/feed.xml/route.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { getPosts } from '../posts/get-posts'

const config = {
title: 'Mohsin Hayat',
siteUrl: 'https://mohsinht.com',
description: 'Writing about engineering, remote work, and software careers.',
language: 'en-us'
}
import { profile, siteUrl } from '../../data/profile'

function escapeXml(value = '') {
return String(value)
Expand All @@ -15,38 +9,31 @@ function escapeXml(value = '') {
.replaceAll('"', '"')
.replaceAll("'", ''')
}

export const dynamic = 'force-static'

export async function GET() {
const posts = await getPosts()
const latest = posts.reduce(
(newest, post) =>
Math.max(
newest,
new Date(
post.frontMatter.dateModified ?? post.frontMatter.date
).getTime()
),
0
)
const items = posts
.map((post) => {
const url = `${config.siteUrl}${post.route}`
return ` <item>
<title>${escapeXml(post.frontMatter.title ?? post.title)}</title>
<description>${escapeXml(post.frontMatter.description)}</description>
<link>${url}</link>
<guid>${url}</guid>
<pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>
</item>`
const url = new URL(post.route, siteUrl).toString()
const categories = (post.frontMatter.tags ?? [])
.map((tag) => `<category>${escapeXml(tag)}</category>`)
.join('')
return ` <item>\n <title>${escapeXml(post.frontMatter.title ?? post.title)}</title>\n <description>${escapeXml(post.frontMatter.description)}</description>\n <link>${url}</link>\n <guid isPermaLink="true">${url}</guid>\n <pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>\n <author>${escapeXml(profile.email)} (${escapeXml(profile.name)})</author>${categories}\n </item>`
})
.join('\n')

const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>${config.title}</title>
<link>${config.siteUrl}</link>
<description>${config.description}</description>
<language>${config.language}</language>
${items}
</channel>
</rss>`

const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeXml(profile.name)}</title>\n <link>${siteUrl}</link>\n <description>Engineering writing by ${escapeXml(profile.name)}.</description>\n <language>en</language>\n <lastBuildDate>${new Date(latest || Date.now()).toUTCString()}</lastBuildDate>\n <atom:link href="${siteUrl}/feed.xml" rel="self" type="application/rss+xml"/>\n${items}\n </channel>\n</rss>`
return new Response(xml, {
headers: {
'Content-Type': 'application/rss+xml; charset=utf-8'
}
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' }
})
}
1 change: 1 addition & 0 deletions app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading