Skip to content

Commit 003a231

Browse files
committed
Added aka logic
1 parent a63e45b commit 003a231

10 files changed

Lines changed: 291 additions & 1 deletion

File tree

.github/workflows/deploy.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Build and deploy Hugo site
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
concurrency:
16+
group: pages
17+
cancel-in-progress: false
18+
19+
jobs:
20+
build:
21+
name: Build Hugo site
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Check out repository
26+
uses: actions/checkout@v6
27+
28+
- name: Set up Node.js
29+
uses: actions/setup-node@v6
30+
with:
31+
node-version: 24
32+
33+
- name: Set up Pages
34+
id: pages
35+
uses: actions/configure-pages@v5
36+
37+
- name: Set up Hugo
38+
uses: peaceiris/actions-hugo@v3.2.0
39+
with:
40+
hugo-version: "0.153.0"
41+
extended: false
42+
43+
- name: Build
44+
run: |
45+
node scripts/prepare-redirects.mjs
46+
hugo --gc --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
47+
48+
- name: Upload Pages artifact
49+
if: github.event_name != 'pull_request'
50+
uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: ./public
53+
54+
deploy:
55+
name: Deploy to GitHub Pages
56+
if: github.event_name != 'pull_request'
57+
needs: build
58+
runs-on: ubuntu-latest
59+
permissions:
60+
contents: read
61+
pages: write
62+
id-token: write
63+
environment:
64+
name: github-pages
65+
url: ${{ steps.deployment.outputs.page_url }}
66+
67+
steps:
68+
- name: Deploy
69+
id: deployment
70+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public/
2+
public-test/
3+
resources/
4+
.generated-content/
5+
.hugo_build.lock

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
11
# aka
2-
A PnP version of aka.ms
2+
3+
A PnP version of aka.ms.
4+
5+
## Add a redirect
6+
7+
Create a Markdown file in `content` named after the short link slug. Add the destination URL in the `url` front matter field.
8+
9+
For example, `content/matrix.md` creates `https://pnp.github.io/aka/matrix/`:
10+
11+
```markdown
12+
---
13+
url: "https://learn.microsoft.com/sharepoint/dev/spfx/compatibility"
14+
---
15+
```
16+
17+
If the URL does not include a scheme, the site redirects to `https://`.
18+
19+
The `url` field is intentionally used only as the redirect destination. A prebuild script converts files from `content` into Hugo-safe generated pages so Hugo does not treat `url` as the page permalink.
20+
21+
Every redirect page automatically renders this telemetry image before redirecting:
22+
23+
```html
24+
<img src="https://pnptelemetry.azurewebsites.net/pnp.github.io/aka/[slug]" alt="">
25+
```
26+
27+
## Deployment
28+
29+
GitHub Actions builds pull requests for validation. Pushes to `main`, including merged pull requests, build and deploy the Hugo site to GitHub Pages at `https://pnp.github.io/aka/`.
30+
31+
Run a local build with:
32+
33+
```shell
34+
npm run build:local
35+
```

archetypes/default.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
url: "https://example.com"
3+
---

content/matrix.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
url: "https://learn.microsoft.com/en-us/sharepoint/dev/spfx/compatibility"
3+
---

hugo.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
baseURL: https://pnp.github.io/aka/
2+
languageCode: en-us
3+
title: PnP Aka
4+
contentDir: .generated-content
5+
6+
disableKinds:
7+
- RSS
8+
- sitemap
9+
- taxonomy
10+
- term
11+
12+
markup:
13+
goldmark:
14+
renderer:
15+
unsafe: true

layouts/_default/single.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{{- $target := .Params.redirect_url -}}
2+
{{- if not $target -}}
3+
{{- errorf "Missing required generated 'redirect_url' front matter in %s" .File.Path -}}
4+
{{- end -}}
5+
{{- $parsedTarget := urls.Parse $target -}}
6+
{{- if not $parsedTarget.Scheme -}}
7+
{{- $target = printf "https://%s" $target -}}
8+
{{- end -}}
9+
{{- $slug := .Params.source_slug | default .File.TranslationBaseName | urlize -}}
10+
{{- $telemetryUrl := printf "https://pnptelemetry.azurewebsites.net/pnp.github.io/aka/%s" $slug -}}
11+
<!doctype html>
12+
<html lang="{{ site.Language.LanguageCode }}">
13+
<head>
14+
<meta charset="utf-8">
15+
<meta name="robots" content="noindex">
16+
<meta http-equiv="refresh" content="1;url={{ $target }}">
17+
<link rel="canonical" href="{{ $target }}">
18+
<title>Redirecting to {{ $target }}</title>
19+
</head>
20+
<body style='font-family: "Segoe UI", "Segoe Sans", sans-serif;'>
21+
<img id="telemetry" src="{{ $telemetryUrl }}" alt="">
22+
<p><a href="{{ $target }}">Continue to link</a></p>
23+
<script>
24+
const target = {{ $target | jsonify | safeJS }};
25+
const image = document.getElementById("telemetry");
26+
let redirected = false;
27+
28+
function redirect() {
29+
if (redirected) {
30+
return;
31+
}
32+
33+
redirected = true;
34+
window.location.replace(target);
35+
}
36+
37+
if (image && image.complete) {
38+
setTimeout(redirect, 0);
39+
} else if (image) {
40+
image.addEventListener("load", redirect, { once: true });
41+
image.addEventListener("error", redirect, { once: true });
42+
setTimeout(redirect, 1000);
43+
} else {
44+
redirect();
45+
}
46+
</script>
47+
</body>
48+
</html>

layouts/home.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<html lang="{{ site.Language.LanguageCode }}">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>{{ site.Title }}</title>
6+
</head>
7+
<body></body>
8+
</html>

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@pnp/aka",
3+
"private": true,
4+
"scripts": {
5+
"build": "node scripts/prepare-redirects.mjs && hugo --gc --minify",
6+
"build:local": "node scripts/prepare-redirects.mjs && hugo --destination public --cleanDestinationDir"
7+
}
8+
}

scripts/prepare-redirects.mjs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2+
import path from "node:path";
3+
4+
const repoRoot = process.cwd();
5+
const sourceDir = path.join(repoRoot, "content");
6+
const outputDir = path.join(repoRoot, ".generated-content");
7+
8+
async function findMarkdownFiles(dir) {
9+
let entries;
10+
11+
try {
12+
entries = await readdir(dir, { withFileTypes: true });
13+
} catch (error) {
14+
if (error.code === "ENOENT") {
15+
return [];
16+
}
17+
18+
throw error;
19+
}
20+
21+
const files = [];
22+
23+
for (const entry of entries) {
24+
const fullPath = path.join(dir, entry.name);
25+
26+
if (entry.isDirectory()) {
27+
files.push(...await findMarkdownFiles(fullPath));
28+
} else if (entry.isFile() && /\.md$/i.test(entry.name)) {
29+
files.push(fullPath);
30+
}
31+
}
32+
33+
return files;
34+
}
35+
36+
function getFrontMatter(markdown, filePath) {
37+
if (!markdown.startsWith("---")) {
38+
throw new Error(`${filePath} must start with YAML front matter.`);
39+
}
40+
41+
const endMatch = markdown.match(/\r?\n---\r?\n/);
42+
43+
if (!endMatch || endMatch.index === undefined) {
44+
throw new Error(`${filePath} must include closing YAML front matter.`);
45+
}
46+
47+
return markdown.slice(3, endMatch.index);
48+
}
49+
50+
function readUrl(frontMatter, filePath) {
51+
const urlMatch = frontMatter.match(/^url:\s*(?<url>.+?)\s*$/m);
52+
const url = urlMatch?.groups?.url
53+
?.replace(/^['"]|['"]$/g, "")
54+
?.trim();
55+
56+
if (!url) {
57+
throw new Error(`${filePath} must include a non-empty 'url' front matter value.`);
58+
}
59+
60+
return url;
61+
}
62+
63+
await rm(outputDir, { recursive: true, force: true });
64+
await mkdir(outputDir, { recursive: true });
65+
66+
const sourceFiles = await findMarkdownFiles(sourceDir);
67+
const slugs = new Set();
68+
69+
for (const sourceFile of sourceFiles) {
70+
const slug = path.basename(sourceFile, path.extname(sourceFile));
71+
72+
if (slug.startsWith("_")) {
73+
continue;
74+
}
75+
76+
if (slugs.has(slug)) {
77+
throw new Error(`Duplicate redirect slug '${slug}'. Markdown file names must be unique.`);
78+
}
79+
80+
slugs.add(slug);
81+
82+
const markdown = await readFile(sourceFile, "utf8");
83+
const frontMatter = getFrontMatter(markdown, sourceFile);
84+
const url = readUrl(frontMatter, sourceFile);
85+
const generatedMarkdown = [
86+
"---",
87+
`title: ${JSON.stringify(slug)}`,
88+
`redirect_url: ${JSON.stringify(url)}`,
89+
`source_slug: ${JSON.stringify(slug)}`,
90+
"---",
91+
""
92+
].join("\n");
93+
94+
await writeFile(path.join(outputDir, `${slug}.md`), generatedMarkdown, "utf8");
95+
}
96+
97+
console.log(`Prepared ${slugs.size} redirect page${slugs.size === 1 ? "" : "s"}.`);

0 commit comments

Comments
 (0)