Skip to content

Commit 554e7db

Browse files
committed
SEO for contributor page
1 parent ff0336d commit 554e7db

File tree

8 files changed

+128
-3
lines changed

8 files changed

+128
-3
lines changed

api/src/app/endpoints.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { GetContributionsResponse } from "src/contribution/types";
2-
import { GetContributorResponse, GetContributorsResponse } from "src/contributor/types";
2+
import {
3+
GetContributorNameResponse,
4+
GetContributorResponse,
5+
GetContributorsResponse,
6+
} from "src/contributor/types";
37
import { GetMilestonesResponse } from "src/milestone/types";
48
import {
59
GetProjectNameResponse,
@@ -35,6 +39,10 @@ export interface Endpoints {
3539
response: GetContributorResponse;
3640
params: { id: string };
3741
};
42+
"api:contributors/:id/name": {
43+
response: GetContributorNameResponse;
44+
params: { id: string };
45+
};
3846
"api:MileStones/dzcode": {
3947
response: GetMilestonesResponse;
4048
};

api/src/contributor/controller.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { Controller, Get, NotFoundError, Param } from "routing-controllers";
22
import { Service } from "typedi";
33

44
import { ContributorRepository } from "./repository";
5-
import { GetContributorResponse, GetContributorsResponse } from "./types";
5+
import {
6+
GetContributorNameResponse,
7+
GetContributorResponse,
8+
GetContributorsResponse,
9+
} from "./types";
610
import { ProjectRepository } from "src/project/repository";
711
import { ContributionRepository } from "src/contribution/repository";
812

@@ -42,4 +46,15 @@ export class ContributorController {
4246
},
4347
};
4448
}
49+
50+
@Get("/:id/name")
51+
public async getContributorName(@Param("id") id: string): Promise<GetContributorNameResponse> {
52+
const contributor = await this.contributorRepository.findName(id);
53+
54+
if (!contributor) throw new NotFoundError("Contributor not found");
55+
56+
return {
57+
contributor,
58+
};
59+
}
4560
}

api/src/contributor/repository.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ import {
1616
export class ContributorRepository {
1717
constructor(private readonly postgresService: PostgresService) {}
1818

19+
public async findName(contributorId: string) {
20+
const statement = sql`
21+
SELECT
22+
${contributorsTable.id},
23+
${contributorsTable.name}
24+
FROM
25+
${contributorsTable}
26+
WHERE
27+
${contributorsTable.id} = ${contributorId}
28+
`;
29+
30+
const raw = await this.postgresService.db.execute(statement);
31+
const entries = Array.from(raw);
32+
const entry = entries[0];
33+
34+
if (!entry) return null;
35+
36+
const unStringifiedRaw = unStringifyDeep(entry);
37+
const camelCased = camelCaseObject(unStringifiedRaw);
38+
return camelCased;
39+
}
40+
1941
public async findForProject(projectId: string) {
2042
const statement = sql`
2143
SELECT

api/src/contributor/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ export interface GetContributorResponse extends GeneralResponse {
2929
contributions: Array<Pick<ContributionEntity, "id" | "title" | "type">>;
3030
};
3131
}
32+
33+
export interface GetContributorNameResponse extends GeneralResponse {
34+
contributor: Pick<ContributorEntity, "name">;
35+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Env, handleContributorRequest } from "handler/contributor";
2+
3+
export const onRequest: PagesFunction<Env> = handleContributorRequest;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Env, handleContributorRequest } from "handler/contributor";
2+
3+
export const onRequest: PagesFunction<Env> = handleContributorRequest;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
declare const htmlTemplate: string; // @ts-expect-error cloudflare converts this to a string using esbuild
2+
import htmlTemplate from "../public/template.html";
3+
declare const notFoundEn: string; // @ts-expect-error cloudflare converts this to a string using esbuild
4+
import notFoundEn from "../public/404.html";
5+
declare const notFoundAr: string; // @ts-expect-error cloudflare converts this to a string using esbuild
6+
import notFoundAr from "../public/ar/404.html";
7+
8+
import { Environment, environments } from "@dzcode.io/utils/dist/config/environment";
9+
import { fsConfig } from "@dzcode.io/utils/dist/config";
10+
import { plainLocalize } from "@dzcode.io/web/dist/components/locale/utils";
11+
import { dictionary, AllDictionaryKeys } from "@dzcode.io/web/dist/components/locale/dictionary";
12+
import { LanguageEntity } from "@dzcode.io/models/dist/language";
13+
import { fetchV2Factory } from "@dzcode.io/utils/dist/fetch/factory";
14+
import { Endpoints } from "@dzcode.io/api/dist/app/endpoints";
15+
16+
export interface Env {
17+
STAGE: Environment;
18+
}
19+
20+
export const handleContributorRequest: PagesFunction<Env> = async (context) => {
21+
let stage = context.env.STAGE;
22+
if (!environments.includes(stage)) {
23+
console.log(`⚠️ No STAGE provided, falling back to "development"`);
24+
stage = "development";
25+
}
26+
27+
const pathName = new URL(context.request.url).pathname;
28+
29+
const languageRegex = /^\/(ar|en)\//i;
30+
const language = (pathName?.match(languageRegex)?.[1]?.toLowerCase() ||
31+
"en") as LanguageEntity["code"];
32+
const notFound = language === "ar" ? notFoundAr : notFoundEn;
33+
34+
const contributorIdRegex = /team\/(.*)/;
35+
const contributorId = pathName?.match(contributorIdRegex)?.[1];
36+
37+
if (!contributorId)
38+
return new Response(notFound, {
39+
headers: { "content-type": "text/html; charset=utf-8" },
40+
status: 404,
41+
});
42+
43+
const localize = (key: AllDictionaryKeys) =>
44+
plainLocalize(dictionary, language, key, "NO-TRANSLATION");
45+
46+
const fullstackConfig = fsConfig(stage);
47+
const fetchV2 = fetchV2Factory<Endpoints>(fullstackConfig);
48+
49+
try {
50+
const { contributor } = await fetchV2("api:contributors/:id/name", {
51+
params: { id: contributorId },
52+
});
53+
const pageTitle = `${localize("contributor-title-pre")} ${contributor.name} ${localize("contributor-title-post")}`;
54+
55+
const newData = htmlTemplate
56+
.replace(/{{template-title}}/g, pageTitle)
57+
.replace(/{{template-description}}/g, localize("team-description"))
58+
.replace(/{{template-lang}}/g, language);
59+
60+
return new Response(newData, { headers: { "content-type": "text/html; charset=utf-8" } });
61+
} catch (error) {
62+
// @TODO-ZM: log error to sentry
63+
console.error(error);
64+
65+
return new Response(notFound, {
66+
headers: { "content-type": "text/html; charset=utf-8" },
67+
status: 404,
68+
});
69+
}
70+
};

web/cloudflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build": "lerna run build:alone [email protected]/web --include-dependencies --stream",
1818
"deploy:prd": "npm run generate:config -- production && wrangler pages deploy --branch main",
1919
"deploy:stg": "npm run generate:config -- staging && wrangler pages deploy --branch main",
20-
"dev": "npm run build && wrangler pages dev . --port 8080",
20+
"dev": "npm run build && wrangler pages dev . --port 8081",
2121
"dev:prepare": "npm run build && cd .. && npm run bundle && npm run pre-deploy",
2222
"generate:config": "npx tsx scripts/generate-config.ts",
2323
"lint:eslint": "eslint --config ../../packages/tooling/eslint.config.mjs",

0 commit comments

Comments
 (0)