@yysng/astro-boilerplate 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/index.js +4 -0
- package/package.json +21 -0
- package/src/components/BaseHead.astro +183 -0
- package/src/components/Footer.astro +7 -0
- package/src/components/FormattedDate.astro +6 -0
- package/src/components/Header.astro +18 -0
- package/src/components/HeaderLink.astro +7 -0
- package/src/components/Image.astro +8 -0
- package/src/components/blog/BlogCard.astro +15 -0
- package/src/config/aeo.config.ts +4 -0
- package/src/config/business.config.ts +45 -0
- package/src/config/geo.config.ts +8 -0
- package/src/config/index.ts +11 -0
- package/src/config/jsonld.config.ts +6 -0
- package/src/config/nav.config.ts +7 -0
- package/src/config/sanity.config.ts +5 -0
- package/src/config/schema.config.ts +16 -0
- package/src/config/seo.config.ts +6 -0
- package/src/config/site.config.ts +10 -0
- package/src/config/theme.config.ts +6 -0
- package/src/env.d.ts +1 -0
- package/src/pages/blog/[slug].astro +23 -0
- package/src/pages/index.astro +11 -0
- package/src/pages/og/[slug].png.tsx +0 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
Here is the clean, production-ready README.md in pure Markdown, ready for copyβpaste into your repo.
|
|
2
|
+
|
|
3
|
+
βΈ»
|
|
4
|
+
|
|
5
|
+
π YY AstroβSanity Boilerplate
|
|
6
|
+
|
|
7
|
+
A multi-site, multi-language, SEO-first Astro boilerplate with a unified JSON-LD @graph engine.
|
|
8
|
+
|
|
9
|
+
This boilerplate is built for large-scale content ecosystems: travel blogs, multi-domain brands, coaching sites, landing pages, and any project using Astro + Sanity with strict SEO and schema requirements.
|
|
10
|
+
|
|
11
|
+
It ensures maximum portability, scalability, and zero duplication across all JSON-LD, SEO, and AEO (Ask-Engine Optimization) definitions.
|
|
12
|
+
|
|
13
|
+
βΈ»
|
|
14
|
+
|
|
15
|
+
β¨ Core Features
|
|
16
|
+
|
|
17
|
+
π§© Architecture
|
|
18
|
+
β’ Astro (static or SSR)
|
|
19
|
+
β’ Cloudflare adapter compatible
|
|
20
|
+
β’ TailwindCSS 3
|
|
21
|
+
β’ Clean, lightweight component system
|
|
22
|
+
β’ Import aliasing for shared boilerplate modules
|
|
23
|
+
|
|
24
|
+
π SEO Engine
|
|
25
|
+
β’ Canonical URLs
|
|
26
|
+
β’ OpenGraph & Twitter metadata
|
|
27
|
+
β’ Hreflang (multi-language & x-default)
|
|
28
|
+
β’ Config-driven default title & meta description
|
|
29
|
+
β’ Site-wide theme color
|
|
30
|
+
β’ Fully centralized BaseHead component
|
|
31
|
+
|
|
32
|
+
π¦ JSON-LD Automation (Advanced)
|
|
33
|
+
|
|
34
|
+
This boilerplate compiles all schema sources into one clean:
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
"@context": "https://schema.org",
|
|
38
|
+
"@graph": []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Schema layers merged automatically:
|
|
42
|
+
1. Global JSON-LD Config
|
|
43
|
+
(jsonld.config.ts) β e.g. WebSite, Organization, Identity schemas
|
|
44
|
+
2. Business Config
|
|
45
|
+
(business.config.ts) β e.g. LocalBusiness, Org, Logo
|
|
46
|
+
3. Page-level Overrides
|
|
47
|
+
(jsonld, faq, breadcrumbs, itemList, etc. passed from page)
|
|
48
|
+
4. Geo Config or Per-Page GEO Override
|
|
49
|
+
Optional GEO injection (if enabled)
|
|
50
|
+
5. SearchAction (AEO)
|
|
51
|
+
Enabled only for homepage unless overridden
|
|
52
|
+
6. Breadcrumb List
|
|
53
|
+
Automated fallback + customizable per page
|
|
54
|
+
7. FAQ Schema
|
|
55
|
+
Automatically expanded into FAQPage β mainEntity[]
|
|
56
|
+
|
|
57
|
+
The result is always:
|
|
58
|
+
β’ Single JSON-LD script
|
|
59
|
+
β’ No duplicates
|
|
60
|
+
β’ Google-valid schema
|
|
61
|
+
β’ Fully consistent across every site that uses this boilerplate
|
|
62
|
+
|
|
63
|
+
βΈ»
|
|
64
|
+
|
|
65
|
+
π Multi-Site & Multi-Language Support
|
|
66
|
+
|
|
67
|
+
This boilerplate supports:
|
|
68
|
+
β’ Multiple domains (e.g., blog.laimi.vn, laimi.com, partner sites)
|
|
69
|
+
β’ Independent SEO & business identity per site
|
|
70
|
+
β’ Per-project configuration overrides
|
|
71
|
+
β’ Automatic hreflang generation
|
|
72
|
+
β’ Vietnamese (vi-VN) + x-default by default (customizable)
|
|
73
|
+
|
|
74
|
+
βΈ»
|
|
75
|
+
|
|
76
|
+
π§± Sanity Integration
|
|
77
|
+
|
|
78
|
+
Included:
|
|
79
|
+
β’ sanityClient.js with safe fallback when ENV missing
|
|
80
|
+
β’ Smooth integration for GROQ queries
|
|
81
|
+
β’ Environment-based configuration (project ID, dataset, API version)
|
|
82
|
+
|
|
83
|
+
Sanity schemas are not included here β each project maintains its own Studio.
|
|
84
|
+
|
|
85
|
+
βΈ»
|
|
86
|
+
|
|
87
|
+
π Boilerplate Folder Structure
|
|
88
|
+
|
|
89
|
+
yy-astro-sanity-boilerplate/
|
|
90
|
+
β
|
|
91
|
+
βββ components/
|
|
92
|
+
β βββ BaseHead.astro # SEO + JSON-LD brain
|
|
93
|
+
β βββ Header.astro
|
|
94
|
+
β βββ Footer.astro
|
|
95
|
+
β βββ utilities...
|
|
96
|
+
β
|
|
97
|
+
βββ lib/
|
|
98
|
+
β βββ sanityClient.js
|
|
99
|
+
β βββ schema/ # (Optional shared schema helpers)
|
|
100
|
+
β
|
|
101
|
+
βββ config/
|
|
102
|
+
β βββ site.config.ts
|
|
103
|
+
β βββ seo.config.ts
|
|
104
|
+
β βββ geo.config.ts
|
|
105
|
+
β βββ aeo.config.ts
|
|
106
|
+
β βββ jsonld.config.ts
|
|
107
|
+
β βββ business.config.ts
|
|
108
|
+
β βββ nav.config.ts
|
|
109
|
+
β βββ theme.config.ts
|
|
110
|
+
β
|
|
111
|
+
βββ README.md
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
βΈ»
|
|
115
|
+
|
|
116
|
+
π§ How Projects Use This Boilerplate
|
|
117
|
+
|
|
118
|
+
Each real project (e.g. blog-phase-3) imports components from the boilerplate:
|
|
119
|
+
|
|
120
|
+
import BaseHead from "@yy/boilerplate/components/BaseHead.astro";
|
|
121
|
+
|
|
122
|
+
Each project defines its own local config:
|
|
123
|
+
|
|
124
|
+
src/config/
|
|
125
|
+
site.config.ts
|
|
126
|
+
seo.config.ts
|
|
127
|
+
jsonld.config.ts
|
|
128
|
+
geo.config.ts
|
|
129
|
+
business.config.ts
|
|
130
|
+
aeo.config.ts
|
|
131
|
+
|
|
132
|
+
This gives you:
|
|
133
|
+
β’ Boilerplate = global rules
|
|
134
|
+
β’ Project = environment-specific values
|
|
135
|
+
(domain, brand name, logos, colors, social links, business details)
|
|
136
|
+
|
|
137
|
+
βΈ»
|
|
138
|
+
|
|
139
|
+
π JSON-LD Architecture Rules (Official)
|
|
140
|
+
|
|
141
|
+
β Rule 1 β Boilerplate NEVER contains real business data
|
|
142
|
+
|
|
143
|
+
Only schema structure and defaults.
|
|
144
|
+
|
|
145
|
+
β Rule 2 β Project config ALWAYS overrides boilerplate defaults
|
|
146
|
+
|
|
147
|
+
Ensures multi-site compatibility.
|
|
148
|
+
|
|
149
|
+
β Rule 3 β Every page must pass only page-specific data
|
|
150
|
+
|
|
151
|
+
(FAQ, breadcrumbs, geo override, itemList, custom jsonld)
|
|
152
|
+
|
|
153
|
+
β Rule 4 β Only ONE WebSite schema is allowed
|
|
154
|
+
|
|
155
|
+
The boilerplate enforces this.
|
|
156
|
+
|
|
157
|
+
β Rule 5 β Organization schema appears ONCE
|
|
158
|
+
|
|
159
|
+
Controlled through projectβs business.config.ts.
|
|
160
|
+
|
|
161
|
+
β Rule 6 β BreadcrumbList only appears when breadcrumbs are passed
|
|
162
|
+
|
|
163
|
+
No duplication, no auto-injection for deep pages.
|
|
164
|
+
|
|
165
|
+
β Rule 7 β FAQ schema appears only when faq[] is provided
|
|
166
|
+
|
|
167
|
+
No empty FAQPage ever injected.
|
|
168
|
+
|
|
169
|
+
βΈ»
|
|
170
|
+
|
|
171
|
+
π§ͺ Example Usage in Pages
|
|
172
|
+
|
|
173
|
+
<BaseHead
|
|
174
|
+
title="Japan Travel Guide"
|
|
175
|
+
description="A complete guide to visiting Japan"
|
|
176
|
+
url="https://blog.laimi.vn/destinations/japan/"
|
|
177
|
+
image="https://cdn.sanity.io/japan.jpg"
|
|
178
|
+
breadcrumbs={[
|
|
179
|
+
{ name: "Trang chα»§", url: "https://blog.laimi.vn" },
|
|
180
|
+
{ name: "Japan", url: "https://blog.laimi.vn/destinations/japan/" }
|
|
181
|
+
]}
|
|
182
|
+
faq={[
|
|
183
|
+
{ q: "Δi NhαΊt mΓΉa nΓ o ΔαΊΉp nhαΊ₯t?", a: "Thu vΓ mΓΉa hoa anh ΔΓ o." },
|
|
184
|
+
{ q: "CΓ³ cαΊ§n visa NhαΊt khΓ΄ng?", a: "Phα»₯ thuα»c quα»c tα»ch." }
|
|
185
|
+
]}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
βΈ»
|
|
190
|
+
|
|
191
|
+
π Environment Variables
|
|
192
|
+
|
|
193
|
+
Your project (not boilerplate) should define:
|
|
194
|
+
|
|
195
|
+
SANITY_PROJECT_ID=
|
|
196
|
+
SANITY_DATASET=
|
|
197
|
+
SANITY_API_VERSION=
|
|
198
|
+
SANITY_READ_TOKEN=
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
βΈ»
|
|
202
|
+
|
|
203
|
+
π Installation
|
|
204
|
+
|
|
205
|
+
1. Add alias inside project astro.config.mjs
|
|
206
|
+
|
|
207
|
+
alias: {
|
|
208
|
+
"@yy/boilerplate": fileURLToPath(
|
|
209
|
+
new URL("../yy-astro-sanity-boilerplate", import.meta.url)
|
|
210
|
+
),
|
|
211
|
+
"@config": "./src/config",
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
2. Import BaseHead in any page
|
|
215
|
+
|
|
216
|
+
import BaseHead from "@yy/boilerplate/components/BaseHead.astro";
|
|
217
|
+
|
|
218
|
+
3. Add your local config overrides in src/config/*
|
|
219
|
+
|
|
220
|
+
βΈ»
|
|
221
|
+
|
|
222
|
+
π§ Philosophy
|
|
223
|
+
|
|
224
|
+
One boilerplate, unlimited sites.
|
|
225
|
+
Project configs can change.
|
|
226
|
+
Brands can change.
|
|
227
|
+
Logos, geo, business details can change.
|
|
228
|
+
|
|
229
|
+
The boilerplate must NEVER change.
|
|
230
|
+
|
|
231
|
+
Everything is fully decoupled so you can:
|
|
232
|
+
β’ Create new microsites fast
|
|
233
|
+
β’ Share the same SEO/JSON-LD engine
|
|
234
|
+
β’ Keep strict consistency across brands
|
|
235
|
+
β’ Avoid technical debt
|
|
236
|
+
|
|
237
|
+
βΈ»
|
|
238
|
+
|
|
239
|
+
π Status
|
|
240
|
+
|
|
241
|
+
This boilerplate now supports:
|
|
242
|
+
β’ β Full JSON-LD architecture
|
|
243
|
+
β’ β No duplication
|
|
244
|
+
β’ β 100/100 validity for schema
|
|
245
|
+
β’ β Multi-site overrides
|
|
246
|
+
β’ β Perfect Lighthouse SEO structure
|
|
247
|
+
β’ β Fully hydrated BaseHead component
|
|
248
|
+
|
|
249
|
+
Ready for Phase 3 integration.
|
|
250
|
+
|
|
251
|
+
βΈ»
|
|
252
|
+
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yysng/astro-boilerplate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Astro + Sanity Boilerplate with AEO Layers 1β5",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./components/*": "./src/components/*",
|
|
8
|
+
"./layouts/*": "./src/layouts/*",
|
|
9
|
+
"./config/*": "./src/config/*",
|
|
10
|
+
"./utils/*": "./src/utils/*"
|
|
11
|
+
},
|
|
12
|
+
"main": "index.js",
|
|
13
|
+
"files": [
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"astro": "^4.0.0",
|
|
18
|
+
"sanity": "^3.20.0",
|
|
19
|
+
"@portabletext/to-html": "^2.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* BaseHead.astro β FINAL BOILERPLATE VERSION (with full GEO support)
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* β Central SEO engine for all boilerplate-based projects
|
|
6
|
+
* β Global JSON-LD (Organization, LocalBusiness, WebSite)
|
|
7
|
+
* β Page-level JSON-LD injection (Article, Country, City, etc.)
|
|
8
|
+
* β Full GEO system: Place + PostalAddress + GeoCoordinates
|
|
9
|
+
* β GEO HTML meta tags (geo.placename, geo.region, geo.position, ICBM)
|
|
10
|
+
* β Clean dedupes for WebSite + Organization
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { siteConfig } from "@config/site.config.ts";
|
|
14
|
+
import { jsonldConfig } from "@config/jsonld.config.ts";
|
|
15
|
+
import { businessConfig } from "@config/business.config.ts";
|
|
16
|
+
import { aeoConfig } from "@config/aeo.config.ts";
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
title = siteConfig.defaultTitle,
|
|
20
|
+
description = siteConfig.defaultDescription,
|
|
21
|
+
image = siteConfig.defaultOgImage,
|
|
22
|
+
canonical = siteConfig.siteUrl,
|
|
23
|
+
|
|
24
|
+
jsonld = null, // page-level schemas
|
|
25
|
+
extraJson = [], // FAQPage, ItemList, etc.
|
|
26
|
+
breadcrumbs = null, // BreadcrumbList (JSON-LD)
|
|
27
|
+
geo = null // { placename, region, latitude, longitude }
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
/* ------------------------------------------------------------
|
|
31
|
+
BUILD JSON-LD GRAPH
|
|
32
|
+
------------------------------------------------------------ */
|
|
33
|
+
let graph = [];
|
|
34
|
+
|
|
35
|
+
/* 1) GLOBAL SCHEMAS */
|
|
36
|
+
for (const key of Object.keys(jsonldConfig)) {
|
|
37
|
+
const cfg = jsonldConfig[key];
|
|
38
|
+
if (cfg.enabled && cfg.schema) graph.push(cfg.schema);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* 2) ORGANIZATION */
|
|
42
|
+
if (businessConfig.organization?.enabled) {
|
|
43
|
+
graph.push(businessConfig.organization.schema);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* 3) LOCAL BUSINESS (optional) */
|
|
47
|
+
if (businessConfig.localBusiness?.enabled) {
|
|
48
|
+
graph.push(businessConfig.localBusiness.schema);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* 4) WEBSITE */
|
|
52
|
+
const webSiteSchema = {
|
|
53
|
+
"@type": "WebSite",
|
|
54
|
+
"@id": `${siteConfig.siteUrl}#website`,
|
|
55
|
+
name: siteConfig.siteName,
|
|
56
|
+
url: siteConfig.siteUrl
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/* Publisher link */
|
|
60
|
+
if (businessConfig.organization?.enabled) {
|
|
61
|
+
webSiteSchema.publisher = {
|
|
62
|
+
"@id":
|
|
63
|
+
businessConfig.organization.schema["@id"] ||
|
|
64
|
+
`${siteConfig.siteUrl}#organization`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* SearchAction only on homepage */
|
|
69
|
+
if (canonical === siteConfig.siteUrl && aeoConfig.enableSearchAction) {
|
|
70
|
+
webSiteSchema.potentialAction = {
|
|
71
|
+
"@type": "SearchAction",
|
|
72
|
+
target: `${siteConfig.siteUrl}${aeoConfig.searchUrl}`,
|
|
73
|
+
"query-input": "required name=search_term_string"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
graph.push(webSiteSchema);
|
|
78
|
+
|
|
79
|
+
/* 5) PAGE-LEVEL JSON-LD */
|
|
80
|
+
if (Array.isArray(jsonld)) graph.push(...jsonld);
|
|
81
|
+
else if (jsonld) graph.push(jsonld);
|
|
82
|
+
|
|
83
|
+
/* 6) BREADCRUMBS */
|
|
84
|
+
if (breadcrumbs?.itemListElement?.length) {
|
|
85
|
+
graph.push({
|
|
86
|
+
"@type": "BreadcrumbList",
|
|
87
|
+
itemListElement: breadcrumbs.itemListElement
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* 7) EXTRA JSON */
|
|
92
|
+
if (Array.isArray(extraJson)) {
|
|
93
|
+
extraJson.forEach((item) => {
|
|
94
|
+
if (item && typeof item === "object") graph.push(item);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* 8) FULL GEO SCHEMA */
|
|
99
|
+
if (geo?.placename) {
|
|
100
|
+
const place = {
|
|
101
|
+
"@type": "Place",
|
|
102
|
+
name: geo.placename
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* Region β ISO country code */
|
|
106
|
+
if (geo.region) {
|
|
107
|
+
place.address = {
|
|
108
|
+
"@type": "PostalAddress",
|
|
109
|
+
addressCountry: geo.region
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Coordinates (optional) */
|
|
114
|
+
if (geo.latitude && geo.longitude) {
|
|
115
|
+
place.geo = {
|
|
116
|
+
"@type": "GeoCoordinates",
|
|
117
|
+
latitude: geo.latitude,
|
|
118
|
+
longitude: geo.longitude
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
graph.push(place);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* 9) REMOVE DUPLICATES */
|
|
126
|
+
graph = graph.filter((item, idx, arr) => {
|
|
127
|
+
const type = item["@type"];
|
|
128
|
+
if (type !== "WebSite" && type !== "Organization") return true;
|
|
129
|
+
|
|
130
|
+
return idx === arr.findIndex((x) => x["@type"] === type);
|
|
131
|
+
});
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
<head>
|
|
135
|
+
<!-- BASIC SEO -->
|
|
136
|
+
<meta charset="utf-8" />
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
138
|
+
|
|
139
|
+
<title>{title}</title>
|
|
140
|
+
<meta name="description" content={description} />
|
|
141
|
+
<link rel="canonical" href={canonical} />
|
|
142
|
+
|
|
143
|
+
<!-- THEME -->
|
|
144
|
+
<meta name="theme-color" content={siteConfig.themeColor} />
|
|
145
|
+
|
|
146
|
+
<!-- OG TAGS -->
|
|
147
|
+
<meta property="og:type" content="website" />
|
|
148
|
+
<meta property="og:title" content={title} />
|
|
149
|
+
<meta property="og:description" content={description} />
|
|
150
|
+
<meta property="og:image" content={image} />
|
|
151
|
+
<meta property="og:url" content={canonical} />
|
|
152
|
+
|
|
153
|
+
<!-- TWITTER -->
|
|
154
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
155
|
+
<meta name="twitter:title" content={title} />
|
|
156
|
+
<meta name="twitter:description" content={description} />
|
|
157
|
+
<meta name="twitter:image" content={image} />
|
|
158
|
+
|
|
159
|
+
<!-- GEO META TAGS -->
|
|
160
|
+
{geo?.placename && (
|
|
161
|
+
<>
|
|
162
|
+
<meta name="geo.placename" content={geo.placename} />
|
|
163
|
+
{geo.region && <meta name="geo.region" content={geo.region} />}
|
|
164
|
+
{geo.latitude && geo.longitude && (
|
|
165
|
+
<>
|
|
166
|
+
<meta name="geo.position" content={`${geo.latitude};${geo.longitude}`} />
|
|
167
|
+
<meta name="ICBM" content={`${geo.latitude}, ${geo.longitude}`} />
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<!-- JSON-LD OUTPUT -->
|
|
174
|
+
{graph.length > 0 && (
|
|
175
|
+
<script
|
|
176
|
+
type="application/ld+json"
|
|
177
|
+
set:html={JSON.stringify({
|
|
178
|
+
"@context": "https://schema.org",
|
|
179
|
+
"@graph": graph
|
|
180
|
+
})}
|
|
181
|
+
></script>
|
|
182
|
+
)}
|
|
183
|
+
</head>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { navConfig } from "src/config/nav.config";
|
|
3
|
+
import HeaderLink from "./HeaderLink.astro";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<header class="w-full py-4 border-b flex items-center justify-between">
|
|
7
|
+
<a href="/" class="text-xl font-bold">
|
|
8
|
+
{navConfig.siteName}
|
|
9
|
+
</a>
|
|
10
|
+
|
|
11
|
+
<nav class="flex gap-4">
|
|
12
|
+
{
|
|
13
|
+
navConfig.links.map(link => (
|
|
14
|
+
<HeaderLink href={link.href} text={link.text} />
|
|
15
|
+
))
|
|
16
|
+
}
|
|
17
|
+
</nav>
|
|
18
|
+
</header>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Image from "../Image.astro";
|
|
3
|
+
import FormattedDate from "../FormattedDate.astro";
|
|
4
|
+
|
|
5
|
+
const { post } = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<article class="border rounded-xl p-4 hover:shadow-sm transition">
|
|
9
|
+
<a href={`/blog/${post.slug}`}>
|
|
10
|
+
<Image src={post.mainImage} alt={post.title} class="w-full rounded" />
|
|
11
|
+
<h3 class="text-lg font-semibold mt-3">{post.title}</h3>
|
|
12
|
+
<p class="text-sm opacity-70 mt-1">{post.excerpt}</p>
|
|
13
|
+
<FormattedDate date={post.publishedAt} />
|
|
14
|
+
</a>
|
|
15
|
+
</article>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const businessConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* ORGANIZATION β enabled manually per project
|
|
4
|
+
*/
|
|
5
|
+
organization: {
|
|
6
|
+
enabled: false,
|
|
7
|
+
schema: {
|
|
8
|
+
"@context": "https://schema.org",
|
|
9
|
+
"@type": "Organization",
|
|
10
|
+
name: "",
|
|
11
|
+
url: "",
|
|
12
|
+
logo: "",
|
|
13
|
+
sameAs: []
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* LOCAL BUSINESS β optional
|
|
19
|
+
*/
|
|
20
|
+
localBusiness: {
|
|
21
|
+
enabled: false,
|
|
22
|
+
schema: {
|
|
23
|
+
"@context": "https://schema.org",
|
|
24
|
+
"@type": "LocalBusiness",
|
|
25
|
+
"name": "",
|
|
26
|
+
"image": "",
|
|
27
|
+
"url": "",
|
|
28
|
+
"telephone": "",
|
|
29
|
+
"address": {
|
|
30
|
+
"@type": "PostalAddress",
|
|
31
|
+
"streetAddress": "",
|
|
32
|
+
"addressLocality": "",
|
|
33
|
+
"addressRegion": "",
|
|
34
|
+
"postalCode": "",
|
|
35
|
+
"addressCountry": ""
|
|
36
|
+
},
|
|
37
|
+
"geo": {
|
|
38
|
+
"@type": "GeoCoordinates",
|
|
39
|
+
"latitude": "",
|
|
40
|
+
"longitude": ""
|
|
41
|
+
},
|
|
42
|
+
"openingHours": []
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./site.config";
|
|
2
|
+
export * from "./seo.config";
|
|
3
|
+
export * from "./nav.config";
|
|
4
|
+
export * from "./theme.config";
|
|
5
|
+
export * from "./sanity.config";
|
|
6
|
+
export * from "./geo.config";
|
|
7
|
+
export * from "./aeo.config";
|
|
8
|
+
export * from "./jsonld.config";
|
|
9
|
+
export * from "./business.config";
|
|
10
|
+
export * from "./schema.config";
|
|
11
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const schemaConfig = {
|
|
2
|
+
breadcrumbs: {
|
|
3
|
+
enabled: false,
|
|
4
|
+
// example: [{ name: "Home", url: "/" }, { name: "Blog", url: "/blog" }]
|
|
5
|
+
items: []
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
faq: {
|
|
9
|
+
enabled: false,
|
|
10
|
+
// example:
|
|
11
|
+
// questions: [
|
|
12
|
+
// { question: "What is X?", answer: "X is..." }
|
|
13
|
+
// ]
|
|
14
|
+
questions: []
|
|
15
|
+
}
|
|
16
|
+
};
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference path="../.astro/types.d.ts" />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BlogPost from "@layouts/BlogPost.astro";
|
|
3
|
+
import { fetchFromSanity } from "@utils/fetchFromSanity";
|
|
4
|
+
|
|
5
|
+
const { slug } = Astro.params;
|
|
6
|
+
|
|
7
|
+
const query = `*[_type=="post" && slug.current==$slug][0]{
|
|
8
|
+
title,
|
|
9
|
+
body,
|
|
10
|
+
"image": mainImage.asset->url,
|
|
11
|
+
publishedAt
|
|
12
|
+
}`;
|
|
13
|
+
|
|
14
|
+
const post = await fetchFromSanity(query, { slug });
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<BlogPost
|
|
18
|
+
title={post.title}
|
|
19
|
+
image={post.image}
|
|
20
|
+
publishedAt={post.publishedAt}
|
|
21
|
+
>
|
|
22
|
+
<div innerHTML={post.body} />
|
|
23
|
+
</BlogPost>
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fetchFromSanity } from "@utils/fetchFromSanity";
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
const posts = await fetchFromSanity(`*[_type=="post"]|order(publishedAt desc)[0...20]{
|
|
5
|
+
title, "slug": slug.current, publishedAt
|
|
6
|
+
}`);
|
|
7
|
+
|
|
8
|
+
const items = posts.map(
|
|
9
|
+
(p) => `
|
|
10
|
+
<item>
|
|
11
|
+
<title>${p.title}</title>
|
|
12
|
+
<link>/blog/${p.slug}</link>
|
|
13
|
+
<pubDate>${new Date(p.publishedAt).toUTCString()}</pubDate>
|
|
14
|
+
</item>`
|
|
15
|
+
).join("");
|
|
16
|
+
|
|
17
|
+
return new Response(
|
|
18
|
+
`<?xml version="1.0"?><rss version="2.0"><channel>${items}</channel></rss>`,
|
|
19
|
+
{ headers: { "Content-Type": "application/xml" } }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { fetchFromSanity } from "@utils/fetchFromSanity";
|
|
2
|
+
import { siteConfig } from "@config/site.config";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const posts = await fetchFromSanity(`*[_type=="post"]{ "slug": slug.current }`);
|
|
6
|
+
|
|
7
|
+
const urls = posts.map(
|
|
8
|
+
(p) => `<url><loc>${siteConfig.siteUrl}/blog/${p.slug}</loc></url>`
|
|
9
|
+
).join("");
|
|
10
|
+
|
|
11
|
+
return new Response(
|
|
12
|
+
`<?xml version="1.0"?><urlset>${urls}</urlset>`,
|
|
13
|
+
{ headers: { "Content-Type": "application/xml" } }
|
|
14
|
+
);
|
|
15
|
+
}
|