Skip to content

Commit 193fdec

Browse files
authored
Merge pull request #6 from mohsinht/feat/improve-seo
feat: production-grade Nextra portfolio upgrade
2 parents 36c4cd5 + f6fa06d commit 193fdec

75 files changed

Lines changed: 7504 additions & 1432 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
open-pull-requests-limit: 5
8+
- package-ecosystem: github-actions
9+
directory: /
10+
schedule:
11+
interval: weekly
12+
open-pull-requests-limit: 3

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Verify
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
permissions:
9+
contents: read
10+
statuses: write
11+
12+
concurrency:
13+
group: verify-${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
verify:
18+
name: Test and build
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 30
21+
steps:
22+
- uses: actions/checkout@v7
23+
24+
- uses: actions/setup-node@v7
25+
with:
26+
node-version-file: .node-version
27+
cache: yarn
28+
29+
- name: Install dependencies
30+
run: yarn install --frozen-lockfile
31+
32+
- name: Lint, format, type-check, and test
33+
run: yarn lint && yarn format:check && yarn typecheck && yarn test:coverage
34+
35+
- name: Build the production application
36+
run: yarn build
37+
38+
- name: Verify build output
39+
run: test -f .next/BUILD_ID
40+
41+
- name: Install Playwright browser
42+
run: yarn playwright install --with-deps chromium
43+
44+
- name: Run browser and accessibility tests
45+
run: yarn test:e2e
46+
47+
- name: Run Lighthouse CI
48+
env:
49+
LHCI_GITHUB_TOKEN: ${{ github.token }}
50+
run: yarn lighthouse

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ typings/
7777

7878
# Next.js build output
7979
.next
80+
.lighthouseci
81+
playwright-report
82+
test-results
8083

8184
# Nuxt.js build / generate output
8285
.nuxt

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ yarn dev
1515

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

18+
## Verification
19+
20+
```bash
21+
yarn test
22+
yarn test:coverage
23+
yarn verify
24+
```
25+
26+
`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.
27+
1828
## How to deploy
1929

2030
This site is deployed like a standard Next.js app on Vercel, but you can deploy anywhere that supports Node.js 24.

app/_meta.global.js

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
11
export default {
2-
index: {
3-
type: 'page',
4-
title: 'About'
5-
},
6-
resume: {
7-
type: 'page',
8-
title: 'Resume'
9-
},
2+
index: { type: 'page', title: 'Home' },
3+
resume: { type: 'page', title: 'Resume' },
4+
recommendations: { type: 'page', title: 'Recommendations' },
105
posts: {
116
type: 'page',
12-
title: 'Posts',
7+
title: 'Writing',
138
items: {
149
'shortlisting-remote-companies': 'Shortlisting Remote Companies',
1510
'working-remotely-from-pakistan': 'Working Remotely from Pakistan',
1611
'apply-for-remote-jobs': 'Applying for Remote Jobs',
1712
'standout-in-your-job-application': 'Stand Out in Your Job Application',
1813
'advises-from-my-mentors': 'Advice from My Mentors'
1914
}
20-
},
21-
more: {
22-
type: 'page',
23-
title: 'More'
2415
}
2516
}

app/agent-markdown/route.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { markdownForPath } from '../../lib/agent-markdown'
2+
import { contentSignal } from '../../data/profile'
3+
4+
export const dynamic = 'force-dynamic'
5+
6+
export async function GET(request) {
7+
const path =
8+
request.headers.get('x-agent-markdown-path') ??
9+
new URL(request.url).searchParams.get('path') ??
10+
'/'
11+
const markdown = await markdownForPath(path)
12+
const headers = {
13+
'Content-Signal': contentSignal,
14+
'Content-Type': 'text/markdown; charset=utf-8',
15+
Vary: 'Accept',
16+
'X-Robots-Tag': 'noindex, follow'
17+
}
18+
19+
if (markdown === null)
20+
return new Response('Not Found\n', { status: 404, headers })
21+
22+
return new Response(markdown, { headers })
23+
}

app/apple-icon.png

38.6 KB
Loading

app/feed.xml/route.js

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { getPosts } from '../posts/get-posts'
2-
3-
const config = {
4-
title: 'Mohsin Hayat',
5-
siteUrl: 'https://mohsinht.com',
6-
description: 'Writing about engineering, remote work, and software careers.',
7-
language: 'en-us'
8-
}
2+
import { profile, siteUrl } from '../../data/profile'
93

104
function escapeXml(value = '') {
115
return String(value)
@@ -15,38 +9,31 @@ function escapeXml(value = '') {
159
.replaceAll('"', '"')
1610
.replaceAll("'", ''')
1711
}
18-
1912
export const dynamic = 'force-static'
2013

2114
export async function GET() {
2215
const posts = await getPosts()
16+
const latest = posts.reduce(
17+
(newest, post) =>
18+
Math.max(
19+
newest,
20+
new Date(
21+
post.frontMatter.dateModified ?? post.frontMatter.date
22+
).getTime()
23+
),
24+
0
25+
)
2326
const items = posts
2427
.map((post) => {
25-
const url = `${config.siteUrl}${post.route}`
26-
return ` <item>
27-
<title>${escapeXml(post.frontMatter.title ?? post.title)}</title>
28-
<description>${escapeXml(post.frontMatter.description)}</description>
29-
<link>${url}</link>
30-
<guid>${url}</guid>
31-
<pubDate>${new Date(post.frontMatter.date).toUTCString()}</pubDate>
32-
</item>`
28+
const url = new URL(post.route, siteUrl).toString()
29+
const categories = (post.frontMatter.tags ?? [])
30+
.map((tag) => `<category>${escapeXml(tag)}</category>`)
31+
.join('')
32+
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>`
3333
})
3434
.join('\n')
35-
36-
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
37-
<rss version="2.0">
38-
<channel>
39-
<title>${config.title}</title>
40-
<link>${config.siteUrl}</link>
41-
<description>${config.description}</description>
42-
<language>${config.language}</language>
43-
${items}
44-
</channel>
45-
</rss>`
46-
35+
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>`
4736
return new Response(xml, {
48-
headers: {
49-
'Content-Type': 'application/rss+xml; charset=utf-8'
50-
}
37+
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' }
5138
})
5239
}

app/icon.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)