优化SEO

This commit is contained in:
2026-07-19 12:06:35 +08:00 Unverified
parent b502d21818
commit 1ba06d3ff3
24 changed files with 224 additions and 34 deletions
+6 -4
View File
@@ -12,7 +12,8 @@
- 留言墙:接入 Twikoo 后可在页面中留言。 - 留言墙:接入 Twikoo 后可在页面中留言。
- 全站搜索:通过 Typesense 搜索并筛选时间线、照片、文章和同学公开资料。 - 全站搜索:通过 Typesense 搜索并筛选时间线、照片、文章和同学公开资料。
- PWA:支持添加到主屏幕,并离线访问站点外壳及已浏览的同源内容。 - PWA:支持添加到主屏幕,并离线访问站点外壳及已浏览的同源内容。
- 字体加密:生产构建会随机混淆页面中的汉字,并生成按需裁剪的 WOFF2 字体,降低静态内容被直接复制或抓取的风险 - SEO:默认生产构建保留可读正文,并生成 canonical、结构化数据、`robots.txt` 与站点地图,便于搜索引擎抓取
- 可选字体保护:需要降低静态内容被直接复制的风险时,可单独执行受保护构建。
## 技术栈 ## 技术栈
@@ -34,14 +35,15 @@ npm run dev
```bash ```bash
npm run dev # 启动开发服务器 npm run dev # 启动开发服务器
npm run build # 构建静态站点到 dist npm run build # 构建适合搜索引擎抓取的静态站点到 dist
npm run build:protected # 可选:构建带汉字字体混淆的版本
npm run preview # 预览构建结果 npm run preview # 预览构建结果
npm run search:sync # 将公开内容同步到 Typesense npm run search:sync # 将公开内容同步到 Typesense
``` ```
`npm run build` 会在 Astro 构建完成后加密页面字体,再生成带内容版本号的 Service Worker,并预缓存页面、样式、脚本、加密字体、应用图标和首页主视觉。照片等大体积资源采用访问后缓存,第三方接口始终走网络。 `npm run build` 会在 Astro 构建完成后生成带内容版本号的 Service Worker,并预缓存页面、样式、脚本、应用图标和首页主视觉。照片等大体积资源采用访问后缓存,第三方接口始终走网络。
字体加密只作用于 `dist` 中的生产页面,不修改源码,也不会影响本地开发。每次构建默认生成新的随机映射;如需可复现产物,可设置 `FONT_ENCRYPTION_SEED``code``pre`、表单属性以及脚本动态生成的内容会保留原文,以免影响复制和交互。字体加密只能提高批量采集成本,不能替代访问控制或版权保护。 `npm run build:protected` 会额外混淆 `dist` 页面中的汉字并生成按需裁剪的 WOFF2 字体。该模式会降低搜索引擎理解正文的能力,不适合需要 SEO 的正式部署。字体混淆不修改源码,也不能替代访问控制或版权保护。
## Typesense 全站搜索 ## Typesense 全站搜索
+1 -1
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "astro/config";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
export default defineConfig({ export default defineConfig({
site: "https://example.com", site: "https://www.2024612.xyz/",
vite: { vite: {
resolve: { resolve: {
alias: { alias: {
+2 -1
View File
@@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build && node scripts/encrypt-font.mjs && node scripts/build-pwa.mjs", "build": "astro build && node scripts/build-pwa.mjs",
"build:protected": "astro build && node scripts/encrypt-font.mjs && node scripts/build-pwa.mjs",
"preview": "astro preview", "preview": "astro preview",
"search:key": "node --env-file-if-exists=.env --env-file-if-exists=.env.local scripts/create-typesense-search-key.mjs", "search:key": "node --env-file-if-exists=.env --env-file-if-exists=.env.local scripts/create-typesense-search-key.mjs",
"search:sync": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/sync-typesense.ts" "search:sync": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --import tsx scripts/sync-typesense.ts"
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"id": "/", "id": "/",
"name": "2024届612班纪念网站", "name": "高平一中612班纪念网站",
"short_name": "612班", "short_name": "612班",
"description": "保存2024届612班共同走过的校园时光,也记录毕业后的我们。", "description": "保存高平一中2024届612班共同走过的校园时光,也记录毕业后的我们。",
"lang": "zh-CN", "lang": "zh-CN",
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
+71
View File
@@ -0,0 +1,71 @@
---
import { site } from "../data/site";
interface Props {
title?: string;
description?: string;
image?: string;
noindex?: boolean;
}
const {
title = site.name,
description = site.description,
image = site.heroImage,
noindex = false
} = Astro.props;
const documentTitle = title === site.name || title.includes(site.name)
? title
: `${title} · ${site.name}`;
const baseUrl = Astro.site ?? new URL(site.url);
const canonicalUrl = new URL(Astro.url.pathname, baseUrl);
const imageUrl = new URL(image, baseUrl);
const robots = noindex
? "noindex, nofollow"
: "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1";
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": `${new URL("/", baseUrl)}#website`,
url: new URL("/", baseUrl).toString(),
name: site.name,
alternateName: [site.shortName, `${site.cohort}${site.shortName}`],
description: site.description,
inLanguage: "zh-CN"
},
{
"@type": "WebPage",
"@id": `${canonicalUrl}#webpage`,
url: canonicalUrl.toString(),
name: documentTitle,
description,
isPartOf: { "@id": `${new URL("/", baseUrl)}#website` },
inLanguage: "zh-CN"
}
]
};
---
<title>{documentTitle}</title>
<meta name="description" content={description} />
<meta name="robots" content={robots} />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content="website" />
<meta property="og:locale" content="zh_CN" />
<meta property="og:site_name" content={site.name} />
<meta property="og:title" content={documentTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={imageUrl} />
<meta property="og:image:alt" content={`${site.name}校园记忆`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={documentTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
<script is:inline type="application/ld+json" set:html={JSON.stringify(structuredData)} />
+1 -1
View File
@@ -907,7 +907,7 @@
"displayDate": "2025.05.28", "displayDate": "2025.05.28",
"sourceUrl": "https://mp.weixin.qq.com/s/BzwaFT3_-lTwkz1keEexZg", "sourceUrl": "https://mp.weixin.qq.com/s/BzwaFT3_-lTwkz1keEexZg",
"cover": "/assets/article-covers/cover-325470909f3b.webp", "cover": "/assets/article-covers/cover-325470909f3b.webp",
"excerpt": "网站群于今日起上线试运营。 共三个站点: 主站点:https://class612.bond", "excerpt": "网站群于今日起上线试运营。 共三个站点: 主站点:https://www.2024612.xyz/",
"category": "校园通知", "category": "校园通知",
"fileName": "网站群上线通知.md", "fileName": "网站群上线通知.md",
"coverSourceUrl": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/RKNa5pic8piampaibDJNlrCiawqhia3LzibyNa3MNNlEAUAOqz6Fxyurqr4ic3YokNjqMPazGibr1vl2lQX67PLwVWWFlg/640?wx_fmt=jpeg" "coverSourceUrl": "https://mmbiz.qpic.cn/sz_mmbiz_jpg/RKNa5pic8piampaibDJNlrCiawqhia3LzibyNa3MNNlEAUAOqz6Fxyurqr4ic3YokNjqMPazGibr1vl2lQX67PLwVWWFlg/640?wx_fmt=jpeg"
+8 -2
View File
@@ -1,11 +1,17 @@
export const site = { export const site = {
className: "2024届612班", name: "高平一中612班",
shortName: "612班",
className: "高平一中612班",
cohort: "2024届",
url: "https://www.2024612.xyz/",
title: "毕业后,我们依然在场", title: "毕业后,我们依然在场",
subtitle: subtitle:
"把散落在相册、聊天记录和心里的瞬间放回同一个地方。这里先留给高中的三年,也留给一年后的我们。", "把散落在相册、聊天记录和心里的瞬间放回同一个地方。这里先留给高中的三年,也留给一年后的我们。",
description:
"高平一中2024届612班纪念网站,收录三年时间线、班级照片、青春刊物和同学近况,保存我们共同走过的高中时光。",
anniversary: "2024.06 - 2026.06 · 毕业周年纪念", anniversary: "2024.06 - 2026.06 · 毕业周年纪念",
heroImage: "/assets/campus-hero.png", heroImage: "/assets/campus-hero.png",
footer: "2024届612班纪念网站" footer: "高平一中612班纪念网站"
}; };
export const contactLinks = [ export const contactLinks = [
+11 -3
View File
@@ -1,13 +1,22 @@
--- ---
import "../styles/global.css"; import "../styles/global.css";
import packageJson from "../../package.json"; import packageJson from "../../package.json";
import SeoHead from "../components/SeoHead.astro";
import { contactLinks, navItems, site } from "../data/site"; import { contactLinks, navItems, site } from "../data/site";
interface Props { interface Props {
title?: string; title?: string;
description?: string;
image?: string;
noindex?: boolean;
} }
const { title = site.title } = Astro.props; const {
title = site.name,
description = site.description,
image = site.heroImage,
noindex = false
} = Astro.props;
const buildTime = new Intl.DateTimeFormat("zh-CN", { const buildTime = new Intl.DateTimeFormat("zh-CN", {
timeZone: "Asia/Shanghai", timeZone: "Asia/Shanghai",
year: "numeric", year: "numeric",
@@ -26,14 +35,13 @@ const buildTime = new Intl.DateTimeFormat("zh-CN", {
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<meta name="description" content={site.subtitle} /> <SeoHead {title} {description} {image} {noindex} />
<meta name="theme-color" content="#156b70" /> <meta name="theme-color" content="#156b70" />
<meta name="application-name" content={site.className} /> <meta name="application-name" content={site.className} />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content={site.className} /> <meta name="apple-mobile-web-app-title" content={site.className} />
<title>{title}</title>
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/png" sizes="192x192" href="/icons/pwa-192.png" /> <link rel="icon" type="image/png" sizes="192x192" href="/icons/pwa-192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
+11 -6
View File
@@ -1,5 +1,7 @@
--- ---
import "../styles/anniversary.css"; import "../styles/anniversary.css";
import SeoHead from "../components/SeoHead.astro";
import { site } from "../data/site";
const memoryCards = [ const memoryCards = [
{ {
@@ -36,6 +38,8 @@ const gallery = [
const backgroundMusic = "/assets/anniversary/background-music.mp3"; const backgroundMusic = "/assets/anniversary/background-music.mp3";
const anniversaryVideo = "/assets/anniversary/anniversary-video.mp4"; const anniversaryVideo = "/assets/anniversary/anniversary-video.mp4";
const videoPoster = "/assets/campus-hero.png"; const videoPoster = "/assets/campus-hero.png";
const anniversaryDescription =
"高平一中2024届612班毕业周年专题,重温三年里的时间切片、班级影像和毕业后的共同回忆。";
--- ---
<!doctype html> <!doctype html>
@@ -43,11 +47,12 @@ const videoPoster = "/assets/campus-hero.png";
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>2024届12班周年专题</title> <SeoHead
<meta title="毕业周年专题"
name="description" description={anniversaryDescription}
content="2024届12班周年专题页,收下那些毕业之后仍然清晰的瞬间。" image={videoPoster}
/> />
<meta name="theme-color" content="#2d433c" />
</head> </head>
<body> <body>
<main class="anniversary-page"> <main class="anniversary-page">
@@ -55,7 +60,7 @@ const videoPoster = "/assets/campus-hero.png";
<img class="hero-media" src="/assets/campus-hero.png" alt="校园记忆" /> <img class="hero-media" src="/assets/campus-hero.png" alt="校园记忆" />
<div class="hero-shade" aria-hidden="true"></div> <div class="hero-shade" aria-hidden="true"></div>
<nav class="topbar" aria-label="周年专题导航"> <nav class="topbar" aria-label="周年专题导航">
<a class="home-link" href="/">2024届12班</a> <a class="home-link" href="/">{site.name}</a>
<div class="topbar-links"> <div class="topbar-links">
<a href="#memory">时间切片</a> <a href="#memory">时间切片</a>
<a href="#gallery">影像回放</a> <a href="#gallery">影像回放</a>
@@ -151,7 +156,7 @@ const videoPoster = "/assets/campus-hero.png";
<h2 id="letter-title">写给毕业后的我们</h2> <h2 id="letter-title">写给毕业后的我们</h2>
<p> <p>
后来的我们会遇见新的城市、新的课程、新的工作和新的自己。可只要有人再次提起那间教室、那次考试、那张合照, 后来的我们会遇见新的城市、新的课程、新的工作和新的自己。可只要有人再次提起那间教室、那次考试、那张合照,
2024届12班就会短暂地重新集合。 高平一中612班就会短暂地重新集合。
</p> </p>
<p> <p>
愿每一次回到这里,都不是为了停在过去,而是确认:那些一起认真生活过的日子,已经成为继续往前走的底气。 愿每一次回到这里,都不是为了停在过去,而是确认:那些一起认真生活过的日子,已经成为继续往前走的底气。
+4 -1
View File
@@ -7,7 +7,10 @@ const categoryCounts = Object.fromEntries(
); );
--- ---
<BaseLayout title="青春刊物 · 2024届612班"> <BaseLayout
title="青春刊物"
description="收录高平一中612班的班级日志、校园时刻、毕业典礼与节日来信,按时间重新翻阅共同的高中记忆。"
>
<main class="page-main publication-page"> <main class="page-main publication-page">
<section class="page-hero publication-hero"> <section class="page-hero publication-hero">
<div class="section-inner"> <div class="section-inner">
+5 -2
View File
@@ -60,7 +60,10 @@ const answerCardQuestions = [
let questionNumber = 0; let questionNumber = 0;
--- ---
<BaseLayout title={`${examIntro.title} · ${site.className}`}> <BaseLayout
title={`${examIntro.title} · ${site.className}`}
description={examIntro.description}
>
<main class="page-main exam-page"> <main class="page-main exam-page">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">
@@ -141,7 +144,7 @@ let questionNumber = 0;
{ {
fillQuestion?.mode === "unordered-names" ? ( fillQuestion?.mode === "unordered-names" ? (
<div class="name-fill-grid" aria-label="第 14 题姓名默写作答"> <div class="name-fill-grid" aria-label="第 14 题姓名默写作答">
{Array.from({ length: fillQuestion.blankCount }, (_, blankIndex) => ( {Array.from({ length: fillQuestion.blankCount ?? 1 }, (_, blankIndex) => (
<label> <label>
<span>{blankIndex + 1}</span> <span>{blankIndex + 1}</span>
<input <input
+5 -1
View File
@@ -3,7 +3,11 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
import { site } from "../../data/site"; import { site } from "../../data/site";
--- ---
<BaseLayout title={`成绩分析 · ${site.className}`}> <BaseLayout
title={`成绩分析 · ${site.className}`}
description="高平一中612班综合素质检测的本地成绩分析页。"
noindex
>
<main class="page-main"> <main class="page-main">
<section class="page-hero result-hero"> <section class="page-hero result-hero">
<div class="section-inner"> <div class="section-inner">
+1 -1
View File
@@ -4,7 +4,7 @@ import { galleryIntro, photoTopics } from "../data/photos";
import { site } from "../data/site"; import { site } from "../data/site";
--- ---
<BaseLayout title={`${galleryIntro.title} · ${site.className}`}> <BaseLayout title={`${galleryIntro.title} · ${site.className}`} description={galleryIntro.text}>
<main class="page-main"> <main class="page-main">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">
+5 -1
View File
@@ -13,7 +13,11 @@ export function getStaticPaths() {
const { topic } = Astro.props; const { topic } = Astro.props;
--- ---
<BaseLayout title={`${topic.title} · ${galleryIntro.title} · ${site.className}`}> <BaseLayout
title={`${topic.title} · ${galleryIntro.title} · ${site.className}`}
description={topic.text}
image={topic.cover || site.heroImage}
>
<main class="page-main"> <main class="page-main">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">
+1 -1
View File
@@ -14,7 +14,7 @@ const previewPhotoTopics = photoTopics.slice(0, 4);
const previewPeople = people.slice(0, 6); const previewPeople = people.slice(0, 6);
--- ---
<BaseLayout title={site.title}> <BaseLayout title={site.name} description={site.description}>
<section <section
id="top" id="top"
class="hero" class="hero"
+4 -1
View File
@@ -72,7 +72,10 @@ const displayCities = layoutCityMarkers();
const isDenseMap = displayCities.length > 12; const isDenseMap = displayCities.length > 12;
--- ---
<BaseLayout title={`班级地图 · ${site.className}`}> <BaseLayout
title={`班级地图 · ${site.className}`}
description="查看高平一中612班同学毕业后的公开城市分布,记录我们从同一间教室走向不同地方的轨迹。"
>
<main class="page-main map-page"> <main class="page-main map-page">
<section class="page-hero map-hero"> <section class="page-hero map-hero">
<div class="section-inner"> <div class="section-inner">
+1 -1
View File
@@ -5,7 +5,7 @@ import { site } from "../data/site";
import twikooThemeUrl from "../styles/twikoo2.css?url"; import twikooThemeUrl from "../styles/twikoo2.css?url";
--- ---
<BaseLayout title={`${messagesIntro.title} · ${site.className}`}> <BaseLayout title={`${messagesIntro.title} · ${site.className}`} description={messagesIntro.text}>
<main class="page-main"> <main class="page-main">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">
+5 -1
View File
@@ -3,7 +3,11 @@ import BaseLayout from "../layouts/BaseLayout.astro";
import { site } from "../data/site"; import { site } from "../data/site";
--- ---
<BaseLayout title={`暂时离线 · ${site.className}`}> <BaseLayout
title={`暂时离线 · ${site.className}`}
description="高平一中612班纪念网站的离线提示页。"
noindex
>
<main class="offline-page"> <main class="offline-page">
<section class="offline-card" aria-labelledby="offline-title"> <section class="offline-card" aria-labelledby="offline-title">
<p class="offline-mark" aria-hidden="true">612</p> <p class="offline-mark" aria-hidden="true">612</p>
+1 -1
View File
@@ -4,7 +4,7 @@ import { people, peopleIntro } from "../data/people";
import { site } from "../data/site"; import { site } from "../data/site";
--- ---
<BaseLayout title={`${peopleIntro.title} · ${site.className}`}> <BaseLayout title={`${peopleIntro.title} · ${site.className}`} description={peopleIntro.text}>
<main class="page-main"> <main class="page-main">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">
+5 -1
View File
@@ -13,7 +13,11 @@ export function getStaticPaths() {
const { person } = Astro.props; const { person } = Astro.props;
--- ---
<BaseLayout title={`${person.name} · ${peopleIntro.title} · ${site.className}`}> <BaseLayout
title={`${person.name} · ${peopleIntro.title} · ${site.className}`}
description={`${person.name}的近况:${person.text}`}
image={person.photo || site.heroImage}
>
<main class="page-main"> <main class="page-main">
<section class="page-hero person-hero"> <section class="page-hero person-hero">
<div class="section-inner person-hero-inner"> <div class="section-inner person-hero-inner">
+19
View File
@@ -0,0 +1,19 @@
import type { APIRoute } from "astro";
import { site as siteData } from "../data/site";
export const prerender = true;
export const GET: APIRoute = ({ site }) => {
const baseUrl = site ?? new URL(siteData.url);
const sitemapUrl = new URL("/sitemap.xml", baseUrl);
const body = [
"User-agent: *",
"Allow: /",
`Sitemap: ${sitemapUrl}`,
""
].join("\n");
return new Response(body, {
headers: { "Content-Type": "text/plain; charset=utf-8" }
});
};
+5 -1
View File
@@ -13,7 +13,11 @@ const typesenseConfig = {
const isConfigured = Boolean(typesenseConfig.host && typesenseConfig.apiKey); const isConfigured = Boolean(typesenseConfig.host && typesenseConfig.apiKey);
--- ---
<BaseLayout title={`全站搜索 · ${site.className}`}> <BaseLayout
title={`全站搜索 · ${site.className}`}
description="搜索高平一中612班的时间线、照片、青春刊物和同学近况。"
noindex
>
<main> <main>
<section class="page-hero search-hero"> <section class="page-hero search-hero">
<div class="section-heading"> <div class="section-heading">
+49
View File
@@ -0,0 +1,49 @@
import type { APIRoute } from "astro";
import { people } from "../data/people";
import { photoTopics } from "../data/photos";
import { site as siteData } from "../data/site";
export const prerender = true;
const staticRoutes = [
"/",
"/timeline/",
"/gallery/",
"/articles/",
"/people/",
"/map/",
"/exam/",
"/messages/",
"/anniversary/"
];
const escapeXml = (value: string) =>
value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");
export const GET: APIRoute = ({ site }) => {
const baseUrl = site ?? new URL(siteData.url);
const routes = [
...staticRoutes,
...photoTopics.map((topic) => `/gallery/${topic.slug}/`),
...people.map((person) => `/people/${person.slug}/`)
];
const urls = [...new Set(routes)]
.map((route) => ` <url><loc>${escapeXml(new URL(route, baseUrl).toString())}</loc></url>`)
.join("\n");
const body = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
urls,
"</urlset>",
""
].join("\n");
return new Response(body, {
headers: { "Content-Type": "application/xml; charset=utf-8" }
});
};
+1 -1
View File
@@ -4,7 +4,7 @@ import { site } from "../data/site";
import { timeline, timelineIntro } from "../data/timeline"; import { timeline, timelineIntro } from "../data/timeline";
--- ---
<BaseLayout title={`${timelineIntro.title} · ${site.className}`}> <BaseLayout title={`${timelineIntro.title} · ${site.className}`} description={timelineIntro.text}>
<main class="page-main"> <main class="page-main">
<section class="page-hero"> <section class="page-hero">
<div class="section-inner"> <div class="section-inner">