Initial commit: AIFO official portal web.

Add static site, shared layout, and Node dev server with standard .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 15:29:50 +08:00
commit 4d30803b60
1154 changed files with 184340 additions and 0 deletions
+238
View File
@@ -0,0 +1,238 @@
/**
* One-time migration: extract nav/footer from index.html, rewrite all HTML pages.
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import crypto from "crypto";
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const COMPONENTS = path.join(ROOT, "components");
const REPORT_PATH = path.join(ROOT, "docs", "layout-migration-report.md");
const SKIP_DIRS = new Set(["temp", "node_modules", "assets", "components", "scripts", "docs"]);
const SKIP_FILES = new Set(["test.html"]);
const NAV_PARTIAL_RE =
/<!--\s*@partial\s+nav\s*-->[\s\S]*?<!--\s*@endpartial\s+nav\s*-->/i;
const NAV_LEGACY_RE =
/<!--\s*fixed-header glass-panel\s*-->\s*<nav id="nav"[\s\S]*?<\/nav>/i;
const NAV_ANY_RE = /<nav id="nav"[\s\S]*?<\/nav>/i;
const FOOTER_RE = /<footer class="pb-3 footer\s+footer-dark">[\s\S]*?<\/footer>/i;
const NAV_CONTAINER = '<div id="nav-container"></div>';
const FOOTER_CONTAINER = '<div id="footer-container"></div>';
const LAYOUT_SCRIPT = '<script src="/js/layout.js"></script>';
function collectHtmlFiles(dir, list = []) {
if (!fs.existsSync(dir)) return list;
for (const name of fs.readdirSync(dir)) {
if (SKIP_DIRS.has(name)) continue;
const full = path.join(dir, name);
const stat = fs.statSync(full);
if (stat.isDirectory()) collectHtmlFiles(full, list);
else if (name.endsWith(".html") && !SKIP_FILES.has(name)) list.push(full);
}
return list;
}
function hashContent(s) {
return crypto.createHash("sha1").update(s).digest("hex").slice(0, 10);
}
function rootifyAssetPaths(html) {
let out = html;
const rules = [
[/src="\.\/assets\//g, 'src="/assets/'],
[/src="\.\.\/assets\//g, 'src="/assets/'],
[/src="assets\//g, 'src="/assets/'],
[/href="\.\/assets\//g, 'href="/assets/'],
[/href="\.\.\/assets\//g, 'href="/assets/'],
[/href="assets\//g, 'href="/assets/'],
[/href="\.\/pages\//g, 'href="/pages/'],
[/href="\.\.\/pages\//g, 'href="/pages/'],
[/href="pages\//g, 'href="/pages/'],
[/href="\.\/index\.html"/g, 'href="/"'],
[/href="\.\.\/index\.html"/g, 'href="/"'],
[/href="\.\/"/g, 'href="/"'],
[/href="\.\.\/"/g, 'href="/"'],
];
for (const [re, rep] of rules) out = out.replace(re, rep);
return out;
}
function extractNavFromIndex(html) {
let m = html.match(NAV_PARTIAL_RE);
if (m) {
let inner = m[0]
.replace(/<!--\s*@partial\s+nav\s*-->/i, "")
.replace(/<!--\s*@endpartial\s+nav\s*-->/i, "")
.trim();
return inner;
}
m = html.match(NAV_LEGACY_RE) || html.match(NAV_ANY_RE);
return m ? m[0].replace(/<!--\s*fixed-header glass-panel\s*-->\s*/i, "").trim() : null;
}
function extractFooterFromIndex(html) {
const m = html.match(FOOTER_RE);
return m ? m[0].trim() : null;
}
function addYearSpan(footerHtml) {
return footerHtml.replace(
/&copy;\s*\d{4}/i,
"&copy; <span id=\"current-year\"></span>"
);
}
function stripNav(html) {
if (NAV_PARTIAL_RE.test(html)) return html.replace(NAV_PARTIAL_RE, NAV_CONTAINER);
if (NAV_LEGACY_RE.test(html)) return html.replace(NAV_LEGACY_RE, NAV_CONTAINER);
if (NAV_ANY_RE.test(html)) return html.replace(NAV_ANY_RE, NAV_CONTAINER);
return html;
}
function stripFooter(html) {
if (!FOOTER_RE.test(html)) return html;
return html.replace(FOOTER_RE, FOOTER_CONTAINER);
}
function ensureLayoutScript(html) {
if (html.includes("/js/layout.js")) return html;
if (html.includes("</body>")) {
return html.replace("</body>", ` ${LAYOUT_SCRIPT}\n</body>`);
}
return html + `\n${LAYOUT_SCRIPT}\n`;
}
function normalizeForCompare(html) {
return html.replace(/\s+/g, " ").trim();
}
function main() {
const indexPath = path.join(ROOT, "index.html");
const indexHtml = fs.readFileSync(indexPath, "utf8");
let nav = extractNavFromIndex(indexHtml);
let footer = extractFooterFromIndex(indexHtml);
if (!nav || !footer) {
console.error("Could not extract nav or footer from index.html");
process.exit(1);
}
nav = rootifyAssetPaths(nav);
footer = addYearSpan(rootifyAssetPaths(footer));
fs.mkdirSync(COMPONENTS, { recursive: true });
fs.mkdirSync(path.join(ROOT, "js"), { recursive: true });
fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
fs.writeFileSync(path.join(COMPONENTS, "nav.html"), nav + "\n", "utf8");
fs.writeFileSync(path.join(COMPONENTS, "footer.html"), footer + "\n", "utf8");
const files = [
indexPath,
...collectHtmlFiles(path.join(ROOT, "pages")),
...collectHtmlFiles(ROOT).filter((f) => f !== indexPath),
];
const uniqueFiles = [...new Set(files)];
const navHashes = new Map();
const footerHashes = new Map();
const report = {
canonicalNavHash: hashContent(normalizeForCompare(nav)),
canonicalFooterHash: hashContent(normalizeForCompare(footer)),
updated: [],
skipped: [],
navVariants: [],
footerVariants: [],
};
for (const file of uniqueFiles) {
let html = fs.readFileSync(file, "utf8");
const rel = path.relative(ROOT, file).replace(/\\/g, "/");
const navMatch =
html.match(NAV_PARTIAL_RE) ||
html.match(NAV_LEGACY_RE) ||
html.match(NAV_ANY_RE);
const footerMatch = html.match(FOOTER_RE);
if (navMatch) {
const h = hashContent(normalizeForCompare(rootifyAssetPaths(navMatch[0])));
navHashes.set(h, (navHashes.get(h) || 0) + 1);
if (h !== report.canonicalNavHash) {
report.navVariants.push({ file: rel, hash: h });
}
}
if (footerMatch) {
const raw = rootifyAssetPaths(footerMatch[0]);
const h = hashContent(normalizeForCompare(raw));
footerHashes.set(h, (footerHashes.get(h) || 0) + 1);
if (h !== report.canonicalFooterHash) {
report.footerVariants.push({ file: rel, hash: h });
}
}
if (!navMatch && !footerMatch) {
report.skipped.push(rel);
continue;
}
let next = html;
if (navMatch) next = stripNav(next);
if (footerMatch) next = stripFooter(next);
if (navMatch || footerMatch) next = ensureLayoutScript(next);
if (next !== html) {
fs.writeFileSync(file, next, "utf8");
report.updated.push(rel);
}
}
const navStats = [...navHashes.entries()].sort((a, b) => b[1] - a[1]);
const footerStats = [...footerHashes.entries()].sort((a, b) => b[1] - a[1]);
const md = `# Layout migration report
Generated: ${new Date().toISOString()}
## Canonical templates (from \`index.html\`)
- Nav hash: \`${report.canonicalNavHash}\`
- Footer hash: \`${report.canonicalFooterHash}\`
## Nav variant counts
${navStats.map(([h, c]) => `- \`${h}\`: ${c} page(s)`).join("\n")}
## Footer variant counts
${footerStats.map(([h, c]) => `- \`${h}\`: ${c} page(s)`).join("\n")}
## Pages with non-canonical nav (${report.navVariants.length})
${report.navVariants.length ? report.navVariants.map((v) => `- ${v.file} (\`${v.hash}\`) — TODO: review if custom nav needed`).join("\n") : "_None_"}
## Pages with non-canonical footer (${report.footerVariants.length})
${report.footerVariants.length ? report.footerVariants.map((v) => `- ${v.file} (\`${v.hash}\`) — TODO: review if custom footer needed`).join("\n") : "_None_"}
## Updated (${report.updated.length})
${report.updated.map((f) => `- ${f}`).join("\n")}
## Skipped (no nav/footer) (${report.skipped.length})
${report.skipped.map((f) => `- ${f}`).join("\n")}
`;
fs.writeFileSync(REPORT_PATH, md, "utf8");
console.log("Wrote components/nav.html, components/footer.html");
console.log("Updated", report.updated.length, "files");
console.log("Report:", REPORT_PATH);
}
main();