@ibalzam/codejitsu-core 0.3.0 → 0.3.2
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/modules/blog/src/collection.d.ts +31 -0
- package/modules/blog/src/collection.js +157 -0
- package/modules/blog/src/collection.ts +1 -1
- package/modules/blog/src/components/index.d.ts +1 -0
- package/modules/blog/src/components/index.js +1 -0
- package/modules/blog/src/fs.d.ts +12 -0
- package/modules/blog/src/fs.js +144 -0
- package/modules/blog/src/index.d.ts +5 -0
- package/modules/blog/src/index.js +3 -0
- package/modules/blog/src/types.d.ts +100 -0
- package/modules/blog/src/types.js +1 -0
- package/modules/blog/src/types.ts +2 -1
- package/modules/config/src/index.d.ts +3 -0
- package/modules/config/src/index.js +4 -0
- package/modules/config/src/types.d.ts +190 -0
- package/modules/config/src/types.js +1 -0
- package/modules/images/src/index.d.ts +3 -0
- package/modules/images/src/index.js +4 -0
- package/modules/seo/src/index.d.ts +2 -0
- package/modules/seo/src/index.js +2 -0
- package/modules/seo/src/schema.d.ts +175 -0
- package/modules/seo/src/schema.js +120 -0
- package/modules/seo/src/sitemap.d.ts +49 -0
- package/modules/seo/src/sitemap.js +81 -0
- package/package.json +32 -10
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BlogCollectionAPI, BlogCollectionEntry, CommonBlogConfig } from './types.js';
|
|
2
|
+
export interface CollectionBlogConfig extends CommonBlogConfig {
|
|
3
|
+
/** Astro Content Collection name. Default 'blog'. */
|
|
4
|
+
collectionName?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Astro Content Collections blog loader. Use this in Astro projects.
|
|
8
|
+
*
|
|
9
|
+
* Returns raw CollectionEntry objects (preserving `entry.data`, `entry.id`,
|
|
10
|
+
* and the ability to call `render(entry)` from astro:content). Filtering and
|
|
11
|
+
* sorting are applied:
|
|
12
|
+
* - Drafts excluded (when `draftField` is set)
|
|
13
|
+
* - Sorted newest first by `dateField`
|
|
14
|
+
* - `getPublishedEntries()` further excludes future-dated entries
|
|
15
|
+
*
|
|
16
|
+
* The collection's actual entry type is `CollectionEntry<'<name>'>` from
|
|
17
|
+
* `astro:content`. Pass it as the generic to get full typed `data`:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import type { CollectionEntry } from 'astro:content';
|
|
21
|
+
* export const blog = createBlogFromCollection<CollectionEntry<'blog'>>({
|
|
22
|
+
* collectionName: 'blog',
|
|
23
|
+
* dateField: 'pubDate',
|
|
24
|
+
* draftField: 'draft',
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Dynamically imports `astro:content` at call time so the package stays
|
|
29
|
+
* usable in non-Astro projects.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createBlogFromCollection<E extends BlogCollectionEntry = BlogCollectionEntry>(config?: CollectionBlogConfig): BlogCollectionAPI<E>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import readingTime from 'reading-time';
|
|
2
|
+
function getTodayUTC() {
|
|
3
|
+
const now = new Date();
|
|
4
|
+
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
|
5
|
+
}
|
|
6
|
+
function asISO(value) {
|
|
7
|
+
if (value instanceof Date)
|
|
8
|
+
return value.toISOString().split('T')[0];
|
|
9
|
+
if (typeof value === 'string')
|
|
10
|
+
return value;
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
function asDate(value) {
|
|
14
|
+
if (value instanceof Date)
|
|
15
|
+
return value;
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
const d = new Date(value);
|
|
18
|
+
return Number.isNaN(d.valueOf()) ? null : d;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Astro Content Collections blog loader. Use this in Astro projects.
|
|
24
|
+
*
|
|
25
|
+
* Returns raw CollectionEntry objects (preserving `entry.data`, `entry.id`,
|
|
26
|
+
* and the ability to call `render(entry)` from astro:content). Filtering and
|
|
27
|
+
* sorting are applied:
|
|
28
|
+
* - Drafts excluded (when `draftField` is set)
|
|
29
|
+
* - Sorted newest first by `dateField`
|
|
30
|
+
* - `getPublishedEntries()` further excludes future-dated entries
|
|
31
|
+
*
|
|
32
|
+
* The collection's actual entry type is `CollectionEntry<'<name>'>` from
|
|
33
|
+
* `astro:content`. Pass it as the generic to get full typed `data`:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import type { CollectionEntry } from 'astro:content';
|
|
37
|
+
* export const blog = createBlogFromCollection<CollectionEntry<'blog'>>({
|
|
38
|
+
* collectionName: 'blog',
|
|
39
|
+
* dateField: 'pubDate',
|
|
40
|
+
* draftField: 'draft',
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Dynamically imports `astro:content` at call time so the package stays
|
|
45
|
+
* usable in non-Astro projects.
|
|
46
|
+
*/
|
|
47
|
+
export function createBlogFromCollection(config = {}) {
|
|
48
|
+
const collectionName = config.collectionName ?? 'blog';
|
|
49
|
+
const defaultAuthor = config.defaultAuthor;
|
|
50
|
+
const categories = config.categories ?? [];
|
|
51
|
+
const dateField = config.dateField ?? 'date';
|
|
52
|
+
const draftField = config.draftField ?? null;
|
|
53
|
+
async function getCollection() {
|
|
54
|
+
let mod;
|
|
55
|
+
try {
|
|
56
|
+
// @ts-expect-error - 'astro:content' is a virtual module resolved by Astro at build time.
|
|
57
|
+
mod = await import('astro:content');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`createBlogFromCollection() requires Astro and a configured content collection ` +
|
|
61
|
+
`named '${collectionName}'. Original error: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
return mod.getCollection(collectionName);
|
|
64
|
+
}
|
|
65
|
+
async function readAll() {
|
|
66
|
+
const all = await getCollection();
|
|
67
|
+
const filtered = draftField ? all.filter((e) => !e.data[draftField]) : all;
|
|
68
|
+
return filtered.sort((a, b) => {
|
|
69
|
+
const da = asDate(a.data[dateField])?.valueOf() ?? 0;
|
|
70
|
+
const db = asDate(b.data[dateField])?.valueOf() ?? 0;
|
|
71
|
+
return db - da;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function getAllEntries() {
|
|
75
|
+
return readAll();
|
|
76
|
+
}
|
|
77
|
+
async function getPublishedEntries() {
|
|
78
|
+
const today = getTodayUTC();
|
|
79
|
+
const all = await readAll();
|
|
80
|
+
return all.filter((e) => {
|
|
81
|
+
const d = asDate(e.data[dateField]);
|
|
82
|
+
return d ? d <= today : true;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function getFutureBlogSlugs() {
|
|
86
|
+
const today = getTodayUTC();
|
|
87
|
+
const all = await readAll();
|
|
88
|
+
return all
|
|
89
|
+
.filter((e) => {
|
|
90
|
+
const d = asDate(e.data[dateField]);
|
|
91
|
+
return d ? d > today : false;
|
|
92
|
+
})
|
|
93
|
+
.map((e) => e.id);
|
|
94
|
+
}
|
|
95
|
+
async function getAllPostSlugs() {
|
|
96
|
+
const all = await readAll();
|
|
97
|
+
return all.map((e) => e.id);
|
|
98
|
+
}
|
|
99
|
+
async function getEntryBySlug(slug) {
|
|
100
|
+
const all = await readAll();
|
|
101
|
+
return all.find((e) => e.id === slug) ?? null;
|
|
102
|
+
}
|
|
103
|
+
async function getAllTags() {
|
|
104
|
+
const entries = await getPublishedEntries();
|
|
105
|
+
const tags = new Set();
|
|
106
|
+
entries.forEach((e) => {
|
|
107
|
+
const t = e.data.tags;
|
|
108
|
+
t?.forEach((tag) => tags.add(tag));
|
|
109
|
+
});
|
|
110
|
+
return Array.from(tags).sort();
|
|
111
|
+
}
|
|
112
|
+
async function getEntriesByTag(tag) {
|
|
113
|
+
const entries = await getPublishedEntries();
|
|
114
|
+
return entries.filter((e) => {
|
|
115
|
+
const t = e.data.tags;
|
|
116
|
+
return t?.includes(tag);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function getAllCategorySlugs() {
|
|
120
|
+
return categories.map((c) => c.slug);
|
|
121
|
+
}
|
|
122
|
+
function getCategoryBySlug(slug) {
|
|
123
|
+
return categories.find((c) => c.slug === slug);
|
|
124
|
+
}
|
|
125
|
+
async function getEntriesByCategory(slug) {
|
|
126
|
+
const cat = getCategoryBySlug(slug);
|
|
127
|
+
if (!cat)
|
|
128
|
+
return [];
|
|
129
|
+
return getEntriesByTag(cat.tag);
|
|
130
|
+
}
|
|
131
|
+
function toMetadata(entry) {
|
|
132
|
+
return {
|
|
133
|
+
slug: entry.id,
|
|
134
|
+
title: entry.data.title ?? '',
|
|
135
|
+
description: entry.data.description ?? '',
|
|
136
|
+
date: asISO(entry.data[dateField]),
|
|
137
|
+
author: entry.data.author ?? defaultAuthor,
|
|
138
|
+
image: entry.data.image,
|
|
139
|
+
tags: entry.data.tags,
|
|
140
|
+
readingTime: readingTime(entry.body ?? '').text,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
getPublishedEntries,
|
|
145
|
+
getAllEntries,
|
|
146
|
+
getFutureBlogSlugs,
|
|
147
|
+
getAllPostSlugs,
|
|
148
|
+
getEntryBySlug,
|
|
149
|
+
getAllTags,
|
|
150
|
+
getEntriesByTag,
|
|
151
|
+
getAllCategorySlugs,
|
|
152
|
+
getCategoryBySlug,
|
|
153
|
+
getEntriesByCategory,
|
|
154
|
+
toMetadata,
|
|
155
|
+
categories,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -165,7 +165,7 @@ export function createBlogFromCollection<E extends BlogCollectionEntry = BlogCol
|
|
|
165
165
|
author: (entry.data.author as string) ?? defaultAuthor,
|
|
166
166
|
image: entry.data.image as string | undefined,
|
|
167
167
|
tags: entry.data.tags as string[] | undefined,
|
|
168
|
-
readingTime: readingTime(entry.body).text,
|
|
168
|
+
readingTime: readingTime(entry.body ?? '').text,
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { BlogPost, BlogPostMetadata, BlogCategory, FAQItem } from '../index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlogFsAPI, CommonBlogConfig } from './types.js';
|
|
2
|
+
export interface FsBlogConfig extends CommonBlogConfig {
|
|
3
|
+
/** Directory of .md files (relative to cwd). Default 'content/blog'. */
|
|
4
|
+
contentDir?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Markdown + gray-matter blog loader. Reads .md files directly from disk.
|
|
8
|
+
* Use this for non-Astro projects, or Astro projects that don't want Content Collections.
|
|
9
|
+
*
|
|
10
|
+
* For Astro projects with Content Collections (recommended), use `createBlogFromCollection`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createBlog(config?: FsBlogConfig): BlogFsAPI;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import readingTime from 'reading-time';
|
|
5
|
+
function getTodayUTC() {
|
|
6
|
+
const now = new Date();
|
|
7
|
+
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
|
8
|
+
}
|
|
9
|
+
function fileSlug(name) {
|
|
10
|
+
return name.replace(/\.md$/, '');
|
|
11
|
+
}
|
|
12
|
+
function asISO(value) {
|
|
13
|
+
if (value instanceof Date)
|
|
14
|
+
return value.toISOString().split('T')[0];
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return value;
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Markdown + gray-matter blog loader. Reads .md files directly from disk.
|
|
21
|
+
* Use this for non-Astro projects, or Astro projects that don't want Content Collections.
|
|
22
|
+
*
|
|
23
|
+
* For Astro projects with Content Collections (recommended), use `createBlogFromCollection`.
|
|
24
|
+
*/
|
|
25
|
+
export function createBlog(config = {}) {
|
|
26
|
+
const contentDir = path.resolve(process.cwd(), config.contentDir ?? 'content/blog');
|
|
27
|
+
const defaultAuthor = config.defaultAuthor;
|
|
28
|
+
const categories = config.categories ?? [];
|
|
29
|
+
const dateField = config.dateField ?? 'date';
|
|
30
|
+
const draftField = config.draftField ?? null;
|
|
31
|
+
function readAllFiles() {
|
|
32
|
+
if (!fs.existsSync(contentDir))
|
|
33
|
+
return [];
|
|
34
|
+
return fs
|
|
35
|
+
.readdirSync(contentDir)
|
|
36
|
+
.filter((n) => n.endsWith('.md'))
|
|
37
|
+
.map((fileName) => {
|
|
38
|
+
const raw = fs.readFileSync(path.join(contentDir, fileName), 'utf8');
|
|
39
|
+
const parsed = matter(raw);
|
|
40
|
+
const data = parsed.data;
|
|
41
|
+
const slug = data.slug || fileSlug(fileName);
|
|
42
|
+
return {
|
|
43
|
+
fileName,
|
|
44
|
+
fileSlug: fileSlug(fileName),
|
|
45
|
+
canonicalSlug: slug,
|
|
46
|
+
data,
|
|
47
|
+
content: parsed.content,
|
|
48
|
+
};
|
|
49
|
+
})
|
|
50
|
+
.filter((f) => (draftField ? !f.data[draftField] : true));
|
|
51
|
+
}
|
|
52
|
+
function toMetadata(f) {
|
|
53
|
+
return {
|
|
54
|
+
slug: f.canonicalSlug,
|
|
55
|
+
title: f.data.title ?? '',
|
|
56
|
+
description: f.data.description ?? '',
|
|
57
|
+
date: asISO(f.data[dateField]),
|
|
58
|
+
author: f.data.author ?? defaultAuthor,
|
|
59
|
+
image: f.data.image,
|
|
60
|
+
tags: f.data.tags,
|
|
61
|
+
readingTime: readingTime(f.content).text,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function getAllPostsIncludingFuture() {
|
|
65
|
+
return readAllFiles()
|
|
66
|
+
.map(toMetadata)
|
|
67
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
68
|
+
}
|
|
69
|
+
async function getAllPosts() {
|
|
70
|
+
const today = getTodayUTC();
|
|
71
|
+
const all = await getAllPostsIncludingFuture();
|
|
72
|
+
return all.filter((p) => new Date(p.date) <= today);
|
|
73
|
+
}
|
|
74
|
+
async function getFutureBlogSlugs() {
|
|
75
|
+
const today = getTodayUTC();
|
|
76
|
+
const slugs = new Set();
|
|
77
|
+
for (const f of readAllFiles()) {
|
|
78
|
+
const date = asISO(f.data[dateField]);
|
|
79
|
+
if (!date)
|
|
80
|
+
continue;
|
|
81
|
+
if (new Date(date) > today) {
|
|
82
|
+
slugs.add(f.canonicalSlug);
|
|
83
|
+
if (f.fileSlug !== f.canonicalSlug)
|
|
84
|
+
slugs.add(f.fileSlug);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Array.from(slugs);
|
|
88
|
+
}
|
|
89
|
+
async function getAllPostSlugs() {
|
|
90
|
+
const slugs = new Set();
|
|
91
|
+
for (const f of readAllFiles()) {
|
|
92
|
+
slugs.add(f.fileSlug);
|
|
93
|
+
if (f.canonicalSlug !== f.fileSlug)
|
|
94
|
+
slugs.add(f.canonicalSlug);
|
|
95
|
+
}
|
|
96
|
+
return Array.from(slugs);
|
|
97
|
+
}
|
|
98
|
+
async function getPostBySlug(slug) {
|
|
99
|
+
const match = readAllFiles().find((f) => f.canonicalSlug === slug || f.fileSlug === slug);
|
|
100
|
+
if (!match)
|
|
101
|
+
return null;
|
|
102
|
+
return {
|
|
103
|
+
...toMetadata(match),
|
|
104
|
+
faqs: match.data.faqs,
|
|
105
|
+
content: match.content,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function getAllTags() {
|
|
109
|
+
const posts = await getAllPosts();
|
|
110
|
+
const tags = new Set();
|
|
111
|
+
posts.forEach((p) => p.tags?.forEach((t) => tags.add(t)));
|
|
112
|
+
return Array.from(tags).sort();
|
|
113
|
+
}
|
|
114
|
+
async function getPostsByTag(tag) {
|
|
115
|
+
const posts = await getAllPosts();
|
|
116
|
+
return posts.filter((p) => p.tags?.includes(tag));
|
|
117
|
+
}
|
|
118
|
+
function getAllCategorySlugs() {
|
|
119
|
+
return categories.map((c) => c.slug);
|
|
120
|
+
}
|
|
121
|
+
function getCategoryBySlug(slug) {
|
|
122
|
+
return categories.find((c) => c.slug === slug);
|
|
123
|
+
}
|
|
124
|
+
async function getPostsByCategory(slug) {
|
|
125
|
+
const cat = getCategoryBySlug(slug);
|
|
126
|
+
if (!cat)
|
|
127
|
+
return [];
|
|
128
|
+
const posts = await getAllPosts();
|
|
129
|
+
return posts.filter((p) => p.tags?.includes(cat.tag));
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
getAllPosts,
|
|
133
|
+
getAllPostsIncludingFuture,
|
|
134
|
+
getFutureBlogSlugs,
|
|
135
|
+
getAllPostSlugs,
|
|
136
|
+
getPostBySlug,
|
|
137
|
+
getAllTags,
|
|
138
|
+
getPostsByTag,
|
|
139
|
+
getAllCategorySlugs,
|
|
140
|
+
getCategoryBySlug,
|
|
141
|
+
getPostsByCategory,
|
|
142
|
+
categories,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface FAQItem {
|
|
2
|
+
question: string;
|
|
3
|
+
answer: string;
|
|
4
|
+
}
|
|
5
|
+
export interface BlogPostFrontmatter {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
date: string | Date;
|
|
9
|
+
slug?: string;
|
|
10
|
+
author?: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
faqs?: FAQItem[];
|
|
14
|
+
draft?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Normalized blog post (fs variant only). */
|
|
18
|
+
export interface BlogPostMetadata {
|
|
19
|
+
slug: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
/** ISO date string (YYYY-MM-DD or full ISO). */
|
|
23
|
+
date: string;
|
|
24
|
+
author?: string;
|
|
25
|
+
image?: string;
|
|
26
|
+
tags?: string[];
|
|
27
|
+
readingTime: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BlogPost extends BlogPostMetadata {
|
|
30
|
+
faqs?: FAQItem[];
|
|
31
|
+
/** Raw markdown body. */
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
export interface BlogCategory {
|
|
35
|
+
slug: string;
|
|
36
|
+
tag: string;
|
|
37
|
+
title: string;
|
|
38
|
+
subtitle: string;
|
|
39
|
+
metaDescription: string;
|
|
40
|
+
}
|
|
41
|
+
export interface CommonBlogConfig {
|
|
42
|
+
defaultAuthor?: string;
|
|
43
|
+
categories?: BlogCategory[];
|
|
44
|
+
/** Frontmatter field name for the date. Default 'date'. */
|
|
45
|
+
dateField?: string;
|
|
46
|
+
/** Frontmatter field name for the draft flag. Default null (no draft support). */
|
|
47
|
+
draftField?: string | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Minimal shape of an Astro CollectionEntry that the package depends on.
|
|
51
|
+
* In modern Astro (5+ with the glob loader), `id` is the slug (filename minus
|
|
52
|
+
* extension). `data` is the parsed frontmatter. `body` is the raw markdown.
|
|
53
|
+
*
|
|
54
|
+
* Sites can cast this to `CollectionEntry<'blog'>` from `astro:content` at use
|
|
55
|
+
* site to get full type information from their CC schema.
|
|
56
|
+
*/
|
|
57
|
+
export interface BlogCollectionEntry {
|
|
58
|
+
id: string;
|
|
59
|
+
data: Record<string, unknown>;
|
|
60
|
+
/** Optional — Astro's CollectionEntry types `body` as `string | undefined`. */
|
|
61
|
+
body?: string;
|
|
62
|
+
}
|
|
63
|
+
/** API for the fs (gray-matter) blog loader — normalized objects. */
|
|
64
|
+
export interface BlogFsAPI {
|
|
65
|
+
getAllPosts(): Promise<BlogPostMetadata[]>;
|
|
66
|
+
getAllPostsIncludingFuture(): Promise<BlogPostMetadata[]>;
|
|
67
|
+
getFutureBlogSlugs(): Promise<string[]>;
|
|
68
|
+
getAllPostSlugs(): Promise<string[]>;
|
|
69
|
+
getPostBySlug(slug: string): Promise<BlogPost | null>;
|
|
70
|
+
getAllTags(): Promise<string[]>;
|
|
71
|
+
getPostsByTag(tag: string): Promise<BlogPostMetadata[]>;
|
|
72
|
+
getAllCategorySlugs(): string[];
|
|
73
|
+
getCategoryBySlug(slug: string): BlogCategory | undefined;
|
|
74
|
+
getPostsByCategory(slug: string): Promise<BlogPostMetadata[]>;
|
|
75
|
+
categories: BlogCategory[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* API for the Astro Content Collections blog loader — raw entries.
|
|
79
|
+
* Preserves full access to entry.data, entry.id, and Astro's `render()`.
|
|
80
|
+
* Filtering (draft + future-date) is applied; sorting is newest-first by `dateField`.
|
|
81
|
+
*/
|
|
82
|
+
export interface BlogCollectionAPI<E extends BlogCollectionEntry = BlogCollectionEntry> {
|
|
83
|
+
/** Published entries: not draft, date <= today. Sorted newest first. */
|
|
84
|
+
getPublishedEntries(): Promise<E[]>;
|
|
85
|
+
/** All non-draft entries (includes future-dated). Sorted newest first. */
|
|
86
|
+
getAllEntries(): Promise<E[]>;
|
|
87
|
+
/** Slugs of non-draft entries with a future date. */
|
|
88
|
+
getFutureBlogSlugs(): Promise<string[]>;
|
|
89
|
+
/** Every slug needed for static path generation (non-draft, includes future). */
|
|
90
|
+
getAllPostSlugs(): Promise<string[]>;
|
|
91
|
+
getEntryBySlug(slug: string): Promise<E | null>;
|
|
92
|
+
getAllTags(): Promise<string[]>;
|
|
93
|
+
getEntriesByTag(tag: string): Promise<E[]>;
|
|
94
|
+
getAllCategorySlugs(): string[];
|
|
95
|
+
getCategoryBySlug(slug: string): BlogCategory | undefined;
|
|
96
|
+
getEntriesByCategory(slug: string): Promise<E[]>;
|
|
97
|
+
/** Convert a CollectionEntry into a normalized BlogPostMetadata. */
|
|
98
|
+
toMetadata(entry: E): BlogPostMetadata;
|
|
99
|
+
categories: BlogCategory[];
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -63,7 +63,8 @@ export interface CommonBlogConfig {
|
|
|
63
63
|
export interface BlogCollectionEntry {
|
|
64
64
|
id: string;
|
|
65
65
|
data: Record<string, unknown>;
|
|
66
|
-
body
|
|
66
|
+
/** Optional — Astro's CollectionEntry types `body` as `string | undefined`. */
|
|
67
|
+
body?: string;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/** API for the fs (gray-matter) blog loader — normalized objects. */
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Codejitsu config — the shape of `codejitsu.config.ts` at site root.
|
|
3
|
+
*
|
|
4
|
+
* One config drives every module. Each top-level module key (`blog`, `seo`,
|
|
5
|
+
* `images`, `llms`, `deploy`) is optional: omit it to disable that module,
|
|
6
|
+
* or include it (even as `{}`) to enable with defaults. Set `enabled: false`
|
|
7
|
+
* to explicitly disable while keeping the section for documentation.
|
|
8
|
+
*/
|
|
9
|
+
export interface CodejitsuConfig {
|
|
10
|
+
/** Site-wide identity and metadata, used by multiple modules. */
|
|
11
|
+
site: SiteConfig;
|
|
12
|
+
blog?: BlogConfig | false;
|
|
13
|
+
seo?: SeoConfig | false;
|
|
14
|
+
images?: ImagesConfig | false;
|
|
15
|
+
llms?: LlmsConfig | false;
|
|
16
|
+
deploy?: DeployConfig | false;
|
|
17
|
+
}
|
|
18
|
+
export interface SiteConfig {
|
|
19
|
+
/** Absolute site URL, no trailing slash. e.g. 'https://example.com'. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Brand name. e.g. 'Pearl Remodeling'. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Appended to <title> tags. e.g. ' — Pearl Remodeling'. */
|
|
24
|
+
titleSuffix?: string;
|
|
25
|
+
/** Default author when blog posts don't specify one. */
|
|
26
|
+
defaultAuthor?: string;
|
|
27
|
+
/** Default OG image (relative path or absolute URL). */
|
|
28
|
+
defaultOgImage?: string;
|
|
29
|
+
/** HTML lang attribute. e.g. 'en-US', 'en'. */
|
|
30
|
+
locale?: string;
|
|
31
|
+
/** Optional structured business info for Organization / LocalBusiness schema. */
|
|
32
|
+
business?: BusinessInfo;
|
|
33
|
+
}
|
|
34
|
+
export interface BusinessInfo {
|
|
35
|
+
legalName?: string;
|
|
36
|
+
telephone?: string;
|
|
37
|
+
email?: string;
|
|
38
|
+
/** Used for LocalBusiness schema and contact pages. */
|
|
39
|
+
address?: PostalAddress;
|
|
40
|
+
geo?: {
|
|
41
|
+
latitude: number;
|
|
42
|
+
longitude: number;
|
|
43
|
+
};
|
|
44
|
+
/** Social profile URLs. */
|
|
45
|
+
sameAs?: string[];
|
|
46
|
+
/** e.g. '$$', '$$$'. */
|
|
47
|
+
priceRange?: string;
|
|
48
|
+
/** Service areas. Strings (city names) or objects. */
|
|
49
|
+
areaServed?: string[];
|
|
50
|
+
/** License number for licensed trades. */
|
|
51
|
+
license?: string;
|
|
52
|
+
/** Schema.org type override, e.g. 'HVACBusiness', 'HomeAndConstructionBusiness'. */
|
|
53
|
+
schemaType?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PostalAddress {
|
|
56
|
+
streetAddress?: string;
|
|
57
|
+
addressLocality: string;
|
|
58
|
+
addressRegion?: string;
|
|
59
|
+
postalCode?: string;
|
|
60
|
+
addressCountry: string;
|
|
61
|
+
}
|
|
62
|
+
export interface BlogConfig {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 'collection' — use Astro Content Collections (recommended for Astro sites).
|
|
66
|
+
* 'fs' — read .md files directly via gray-matter (for non-Astro projects).
|
|
67
|
+
* Defaults to 'collection' if the site has astro as a dep; otherwise 'fs'.
|
|
68
|
+
*/
|
|
69
|
+
mode?: 'collection' | 'fs';
|
|
70
|
+
/** For 'fs' mode: where the .md files live. Default 'content/blog'. */
|
|
71
|
+
contentDir?: string;
|
|
72
|
+
/** For 'collection' mode: name of the Astro CC. Default 'blog'. */
|
|
73
|
+
collectionName?: string;
|
|
74
|
+
/** Frontmatter field for the post date. Default 'date'. Pearl uses 'pubDate'. */
|
|
75
|
+
dateField?: string;
|
|
76
|
+
/** Frontmatter field for draft state. Default null (no draft support). Pearl uses 'draft'. */
|
|
77
|
+
draftField?: string | null;
|
|
78
|
+
/** Category definitions for /blog/category/[slug] pages. */
|
|
79
|
+
categories?: BlogCategory[];
|
|
80
|
+
}
|
|
81
|
+
export interface BlogCategory {
|
|
82
|
+
slug: string;
|
|
83
|
+
tag: string;
|
|
84
|
+
title: string;
|
|
85
|
+
subtitle: string;
|
|
86
|
+
metaDescription: string;
|
|
87
|
+
}
|
|
88
|
+
export interface SeoConfig {
|
|
89
|
+
enabled?: boolean;
|
|
90
|
+
sitemap?: {
|
|
91
|
+
/** Regex patterns to exclude from the sitemap. */
|
|
92
|
+
excludePatterns?: RegExp[];
|
|
93
|
+
/**
|
|
94
|
+
* Site-specific priority rules, evaluated before defaults.
|
|
95
|
+
* First matching pattern wins.
|
|
96
|
+
*/
|
|
97
|
+
priorityRules?: Array<{
|
|
98
|
+
pattern: RegExp;
|
|
99
|
+
priority: number;
|
|
100
|
+
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Default schemas to inject site-wide when not overridden per-page.
|
|
105
|
+
* e.g. always-on Organization or LocalBusiness on every page.
|
|
106
|
+
*/
|
|
107
|
+
defaultSchemas?: ('organization' | 'localBusiness' | 'website')[];
|
|
108
|
+
}
|
|
109
|
+
export interface ImagesConfig {
|
|
110
|
+
enabled?: boolean;
|
|
111
|
+
/** Source dir for the recursive optimizer. Default 'public/images'. */
|
|
112
|
+
sourceDir?: string;
|
|
113
|
+
/** Thumbnail output dir. Set to null to disable thumb generation. Default null. */
|
|
114
|
+
thumbDir?: string | null;
|
|
115
|
+
defaultQuality?: number;
|
|
116
|
+
defaultMaxSize?: number;
|
|
117
|
+
thumbSize?: number;
|
|
118
|
+
thumbQuality?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Per-file rule overrides. Key = path relative to sourceDir without extension.
|
|
121
|
+
* e.g. 'logos/logo': { maxWidth: 329, quality: 35, generateAvif: true }
|
|
122
|
+
*/
|
|
123
|
+
specialRules?: Record<string, SpecialRule>;
|
|
124
|
+
/**
|
|
125
|
+
* Blog-post image automation. Replaces hand-maintained title→slug maps.
|
|
126
|
+
* If set, the optimizer scans `contentDir` for post filenames and looks for
|
|
127
|
+
* matching source images in `sourceImageDir`, optimizing them into `outputDir`.
|
|
128
|
+
*/
|
|
129
|
+
autoBlogImages?: {
|
|
130
|
+
contentDir: string;
|
|
131
|
+
sourceImageDir: string;
|
|
132
|
+
outputDir: string;
|
|
133
|
+
width: number;
|
|
134
|
+
height?: number | null;
|
|
135
|
+
quality?: number;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export interface SpecialRule {
|
|
139
|
+
maxWidth?: number | null;
|
|
140
|
+
maxHeight?: number | null;
|
|
141
|
+
quality?: number;
|
|
142
|
+
smartSubsample?: boolean;
|
|
143
|
+
generateAvif?: boolean;
|
|
144
|
+
optimizePng?: boolean;
|
|
145
|
+
}
|
|
146
|
+
export interface LlmsConfig {
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* 'config' — sections are listed explicitly in this config (simplest sites).
|
|
150
|
+
* 'content-scan' — modules scan content dirs to enumerate URLs (pearl pattern).
|
|
151
|
+
* Default 'config'.
|
|
152
|
+
*/
|
|
153
|
+
mode?: 'config' | 'content-scan';
|
|
154
|
+
/** One-line tagline appended to the title in the output. */
|
|
155
|
+
tagline?: string;
|
|
156
|
+
/** Short "About" paragraph (used in llms.txt). */
|
|
157
|
+
about?: string;
|
|
158
|
+
/** Longer "About" content (used in llms-full.txt; falls back to `about`). */
|
|
159
|
+
aboutFull?: string;
|
|
160
|
+
/** Sections for 'config' mode. Ignored in 'content-scan' mode. */
|
|
161
|
+
sections?: LlmsSection[];
|
|
162
|
+
/** "For AI Assistants" block content (both modes). */
|
|
163
|
+
aiGuidance?: string;
|
|
164
|
+
/** Blog directory (auto-included in both modes). */
|
|
165
|
+
blogDir?: string;
|
|
166
|
+
blogLimit?: number;
|
|
167
|
+
blogFullLimit?: number;
|
|
168
|
+
/** Settings for 'content-scan' mode. */
|
|
169
|
+
contentScan?: {
|
|
170
|
+
servicesDir?: string;
|
|
171
|
+
locationsDir?: string;
|
|
172
|
+
pagesDir?: string;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export interface LlmsSection {
|
|
176
|
+
title: string;
|
|
177
|
+
description?: string;
|
|
178
|
+
items: LlmsSectionItem[];
|
|
179
|
+
}
|
|
180
|
+
export interface LlmsSectionItem {
|
|
181
|
+
title: string;
|
|
182
|
+
description: string;
|
|
183
|
+
url: string;
|
|
184
|
+
fullDescription?: string;
|
|
185
|
+
}
|
|
186
|
+
export interface DeployConfig {
|
|
187
|
+
enabled?: boolean;
|
|
188
|
+
/** Cloudflare Pages project name. Used for documentation only. */
|
|
189
|
+
cloudflarePagesName?: string;
|
|
190
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD schema.org builders. Each function returns a plain object ready
|
|
3
|
+
* to `JSON.stringify` and inject into a `<script type="application/ld+json">`.
|
|
4
|
+
*
|
|
5
|
+
* Helper at the bottom: `jsonLd(obj)` returns the stringified script body.
|
|
6
|
+
*/
|
|
7
|
+
export interface OrganizationInput {
|
|
8
|
+
name: string;
|
|
9
|
+
url: string;
|
|
10
|
+
logo?: string;
|
|
11
|
+
sameAs?: string[];
|
|
12
|
+
email?: string;
|
|
13
|
+
telephone?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function organization(input: OrganizationInput): {
|
|
16
|
+
telephone?: string | undefined;
|
|
17
|
+
email?: string | undefined;
|
|
18
|
+
sameAs?: string[] | undefined;
|
|
19
|
+
logo?: string | undefined;
|
|
20
|
+
'@context': string;
|
|
21
|
+
'@type': string;
|
|
22
|
+
name: string;
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
export interface PostalAddress {
|
|
26
|
+
streetAddress?: string;
|
|
27
|
+
addressLocality: string;
|
|
28
|
+
addressRegion?: string;
|
|
29
|
+
postalCode?: string;
|
|
30
|
+
addressCountry: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GeoCoordinates {
|
|
33
|
+
latitude: number;
|
|
34
|
+
longitude: number;
|
|
35
|
+
}
|
|
36
|
+
export interface LocalBusinessInput {
|
|
37
|
+
name: string;
|
|
38
|
+
url: string;
|
|
39
|
+
telephone: string;
|
|
40
|
+
image?: string;
|
|
41
|
+
priceRange?: string;
|
|
42
|
+
address: PostalAddress;
|
|
43
|
+
geo?: GeoCoordinates;
|
|
44
|
+
openingHours?: string[];
|
|
45
|
+
areaServed?: string[];
|
|
46
|
+
sameAs?: string[];
|
|
47
|
+
/** Override @type to a more specific LocalBusiness subtype, e.g. 'HVACBusiness', 'HomeAndConstructionBusiness'. */
|
|
48
|
+
type?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function localBusiness(input: LocalBusinessInput): {
|
|
51
|
+
sameAs?: string[] | undefined;
|
|
52
|
+
areaServed?: string[] | undefined;
|
|
53
|
+
openingHoursSpecification?: string[] | undefined;
|
|
54
|
+
geo?: {
|
|
55
|
+
latitude: number;
|
|
56
|
+
longitude: number;
|
|
57
|
+
'@type': string;
|
|
58
|
+
} | undefined;
|
|
59
|
+
address: {
|
|
60
|
+
streetAddress?: string;
|
|
61
|
+
addressLocality: string;
|
|
62
|
+
addressRegion?: string;
|
|
63
|
+
postalCode?: string;
|
|
64
|
+
addressCountry: string;
|
|
65
|
+
'@type': string;
|
|
66
|
+
};
|
|
67
|
+
priceRange?: string | undefined;
|
|
68
|
+
image?: string | undefined;
|
|
69
|
+
'@context': string;
|
|
70
|
+
'@type': string;
|
|
71
|
+
name: string;
|
|
72
|
+
url: string;
|
|
73
|
+
telephone: string;
|
|
74
|
+
};
|
|
75
|
+
export interface WebSiteInput {
|
|
76
|
+
name: string;
|
|
77
|
+
url: string;
|
|
78
|
+
/** If set, adds SearchAction pointing to this URL template (with `{search_term_string}`). */
|
|
79
|
+
searchUrlTemplate?: string;
|
|
80
|
+
}
|
|
81
|
+
export declare function website(input: WebSiteInput): Record<string, unknown>;
|
|
82
|
+
export interface BlogPostingInput {
|
|
83
|
+
title: string;
|
|
84
|
+
description: string;
|
|
85
|
+
url: string;
|
|
86
|
+
datePublished: string;
|
|
87
|
+
dateModified?: string;
|
|
88
|
+
image?: string;
|
|
89
|
+
authorName?: string;
|
|
90
|
+
publisherName: string;
|
|
91
|
+
publisherLogo?: string;
|
|
92
|
+
}
|
|
93
|
+
export declare function blogPosting(input: BlogPostingInput): {
|
|
94
|
+
publisher: {
|
|
95
|
+
logo?: {
|
|
96
|
+
'@type': string;
|
|
97
|
+
url: string;
|
|
98
|
+
} | undefined;
|
|
99
|
+
'@type': string;
|
|
100
|
+
name: string;
|
|
101
|
+
};
|
|
102
|
+
author?: {
|
|
103
|
+
'@type': string;
|
|
104
|
+
name: string;
|
|
105
|
+
} | undefined;
|
|
106
|
+
image?: string | undefined;
|
|
107
|
+
'@context': string;
|
|
108
|
+
'@type': string;
|
|
109
|
+
headline: string;
|
|
110
|
+
description: string;
|
|
111
|
+
mainEntityOfPage: {
|
|
112
|
+
'@type': string;
|
|
113
|
+
'@id': string;
|
|
114
|
+
};
|
|
115
|
+
url: string;
|
|
116
|
+
datePublished: string;
|
|
117
|
+
dateModified: string;
|
|
118
|
+
};
|
|
119
|
+
export interface FAQ {
|
|
120
|
+
question: string;
|
|
121
|
+
answer: string;
|
|
122
|
+
}
|
|
123
|
+
export declare function faqPage(faqs: FAQ[]): {
|
|
124
|
+
'@context': string;
|
|
125
|
+
'@type': string;
|
|
126
|
+
mainEntity: {
|
|
127
|
+
'@type': string;
|
|
128
|
+
name: string;
|
|
129
|
+
acceptedAnswer: {
|
|
130
|
+
'@type': string;
|
|
131
|
+
text: string;
|
|
132
|
+
};
|
|
133
|
+
}[];
|
|
134
|
+
};
|
|
135
|
+
export interface BreadcrumbItem {
|
|
136
|
+
name: string;
|
|
137
|
+
url: string;
|
|
138
|
+
}
|
|
139
|
+
export declare function breadcrumbList(items: BreadcrumbItem[]): {
|
|
140
|
+
'@context': string;
|
|
141
|
+
'@type': string;
|
|
142
|
+
itemListElement: {
|
|
143
|
+
'@type': string;
|
|
144
|
+
position: number;
|
|
145
|
+
name: string;
|
|
146
|
+
item: string;
|
|
147
|
+
}[];
|
|
148
|
+
};
|
|
149
|
+
export interface ServiceInput {
|
|
150
|
+
name: string;
|
|
151
|
+
description: string;
|
|
152
|
+
url: string;
|
|
153
|
+
provider: {
|
|
154
|
+
name: string;
|
|
155
|
+
url: string;
|
|
156
|
+
};
|
|
157
|
+
areaServed?: string[];
|
|
158
|
+
serviceType?: string;
|
|
159
|
+
}
|
|
160
|
+
export declare function service(input: ServiceInput): {
|
|
161
|
+
serviceType?: string | undefined;
|
|
162
|
+
areaServed?: string[] | undefined;
|
|
163
|
+
'@context': string;
|
|
164
|
+
'@type': string;
|
|
165
|
+
name: string;
|
|
166
|
+
description: string;
|
|
167
|
+
url: string;
|
|
168
|
+
provider: {
|
|
169
|
+
'@type': string;
|
|
170
|
+
name: string;
|
|
171
|
+
url: string;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
/** Stringify a schema object for safe injection into <script type="application/ld+json">. */
|
|
175
|
+
export declare function jsonLd(schema: unknown): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD schema.org builders. Each function returns a plain object ready
|
|
3
|
+
* to `JSON.stringify` and inject into a `<script type="application/ld+json">`.
|
|
4
|
+
*
|
|
5
|
+
* Helper at the bottom: `jsonLd(obj)` returns the stringified script body.
|
|
6
|
+
*/
|
|
7
|
+
export function organization(input) {
|
|
8
|
+
return {
|
|
9
|
+
'@context': 'https://schema.org',
|
|
10
|
+
'@type': 'Organization',
|
|
11
|
+
name: input.name,
|
|
12
|
+
url: input.url,
|
|
13
|
+
...(input.logo && { logo: input.logo }),
|
|
14
|
+
...(input.sameAs && { sameAs: input.sameAs }),
|
|
15
|
+
...(input.email && { email: input.email }),
|
|
16
|
+
...(input.telephone && { telephone: input.telephone }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function localBusiness(input) {
|
|
20
|
+
return {
|
|
21
|
+
'@context': 'https://schema.org',
|
|
22
|
+
'@type': input.type ?? 'LocalBusiness',
|
|
23
|
+
name: input.name,
|
|
24
|
+
url: input.url,
|
|
25
|
+
telephone: input.telephone,
|
|
26
|
+
...(input.image && { image: input.image }),
|
|
27
|
+
...(input.priceRange && { priceRange: input.priceRange }),
|
|
28
|
+
address: { '@type': 'PostalAddress', ...input.address },
|
|
29
|
+
...(input.geo && { geo: { '@type': 'GeoCoordinates', ...input.geo } }),
|
|
30
|
+
...(input.openingHours && { openingHoursSpecification: input.openingHours }),
|
|
31
|
+
...(input.areaServed && { areaServed: input.areaServed }),
|
|
32
|
+
...(input.sameAs && { sameAs: input.sameAs }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function website(input) {
|
|
36
|
+
const base = {
|
|
37
|
+
'@context': 'https://schema.org',
|
|
38
|
+
'@type': 'WebSite',
|
|
39
|
+
name: input.name,
|
|
40
|
+
url: input.url,
|
|
41
|
+
};
|
|
42
|
+
if (input.searchUrlTemplate) {
|
|
43
|
+
base.potentialAction = {
|
|
44
|
+
'@type': 'SearchAction',
|
|
45
|
+
target: { '@type': 'EntryPoint', urlTemplate: input.searchUrlTemplate },
|
|
46
|
+
'query-input': 'required name=search_term_string',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
export function blogPosting(input) {
|
|
52
|
+
return {
|
|
53
|
+
'@context': 'https://schema.org',
|
|
54
|
+
'@type': 'BlogPosting',
|
|
55
|
+
headline: input.title,
|
|
56
|
+
description: input.description,
|
|
57
|
+
mainEntityOfPage: { '@type': 'WebPage', '@id': input.url },
|
|
58
|
+
url: input.url,
|
|
59
|
+
datePublished: input.datePublished,
|
|
60
|
+
dateModified: input.dateModified ?? input.datePublished,
|
|
61
|
+
...(input.image && { image: input.image }),
|
|
62
|
+
...(input.authorName && {
|
|
63
|
+
author: { '@type': 'Person', name: input.authorName },
|
|
64
|
+
}),
|
|
65
|
+
publisher: {
|
|
66
|
+
'@type': 'Organization',
|
|
67
|
+
name: input.publisherName,
|
|
68
|
+
...(input.publisherLogo && {
|
|
69
|
+
logo: { '@type': 'ImageObject', url: input.publisherLogo },
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function faqPage(faqs) {
|
|
75
|
+
return {
|
|
76
|
+
'@context': 'https://schema.org',
|
|
77
|
+
'@type': 'FAQPage',
|
|
78
|
+
mainEntity: faqs.map((faq) => ({
|
|
79
|
+
'@type': 'Question',
|
|
80
|
+
name: faq.question,
|
|
81
|
+
acceptedAnswer: {
|
|
82
|
+
'@type': 'Answer',
|
|
83
|
+
text: faq.answer,
|
|
84
|
+
},
|
|
85
|
+
})),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function breadcrumbList(items) {
|
|
89
|
+
return {
|
|
90
|
+
'@context': 'https://schema.org',
|
|
91
|
+
'@type': 'BreadcrumbList',
|
|
92
|
+
itemListElement: items.map((item, i) => ({
|
|
93
|
+
'@type': 'ListItem',
|
|
94
|
+
position: i + 1,
|
|
95
|
+
name: item.name,
|
|
96
|
+
item: item.url,
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function service(input) {
|
|
101
|
+
return {
|
|
102
|
+
'@context': 'https://schema.org',
|
|
103
|
+
'@type': 'Service',
|
|
104
|
+
name: input.name,
|
|
105
|
+
description: input.description,
|
|
106
|
+
url: input.url,
|
|
107
|
+
provider: {
|
|
108
|
+
'@type': 'Organization',
|
|
109
|
+
name: input.provider.name,
|
|
110
|
+
url: input.provider.url,
|
|
111
|
+
},
|
|
112
|
+
...(input.areaServed && { areaServed: input.areaServed }),
|
|
113
|
+
...(input.serviceType && { serviceType: input.serviceType }),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Stringify a schema object for safe injection into <script type="application/ld+json">. */
|
|
117
|
+
export function jsonLd(schema) {
|
|
118
|
+
// Escape `</` to prevent breakout from the script tag.
|
|
119
|
+
return JSON.stringify(schema).replace(/<\//g, '<\\/');
|
|
120
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the `@astrojs/sitemap` integration.
|
|
3
|
+
*
|
|
4
|
+
* Two complementary functions:
|
|
5
|
+
* - `defaultPriorityRules(site)` produces a `serialize` function that sets
|
|
6
|
+
* priority and changefreq based on URL shape (home, top-level hubs,
|
|
7
|
+
* service/area pages, blog posts).
|
|
8
|
+
* - `excludeFuturePosts(futureSlugs)` produces a `filter` function that
|
|
9
|
+
* drops scheduled blog posts whose slugs are in the supplied set.
|
|
10
|
+
*
|
|
11
|
+
* Compose both like:
|
|
12
|
+
*
|
|
13
|
+
* sitemap({
|
|
14
|
+
* filter: excludeFuturePosts(await blog.getFutureBlogSlugs()),
|
|
15
|
+
* serialize: defaultPriorityRules(SITE),
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
export interface SitemapItem {
|
|
19
|
+
url: string;
|
|
20
|
+
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
|
21
|
+
priority?: number;
|
|
22
|
+
lastmod?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PriorityRulesOptions {
|
|
25
|
+
/** Extra patterns appended to the defaults. Earlier entries win. */
|
|
26
|
+
rules?: Array<{
|
|
27
|
+
pattern: RegExp;
|
|
28
|
+
priority: number;
|
|
29
|
+
changefreq?: SitemapItem['changefreq'];
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns a `serialize` function that assigns priority/changefreq based on
|
|
34
|
+
* URL shape. Tune via `options.rules` for site-specific top-level paths.
|
|
35
|
+
*
|
|
36
|
+
* @param site The site origin (no trailing slash), e.g. 'https://example.com'.
|
|
37
|
+
*/
|
|
38
|
+
export declare function defaultPriorityRules(site: string, options?: PriorityRulesOptions): (item: SitemapItem) => SitemapItem;
|
|
39
|
+
/**
|
|
40
|
+
* Returns a `filter` function for `@astrojs/sitemap` that excludes any URL
|
|
41
|
+
* whose final path segment matches a future-dated blog slug.
|
|
42
|
+
*
|
|
43
|
+
* Pass `await blog.getFutureBlogSlugs()` to the constructor.
|
|
44
|
+
*/
|
|
45
|
+
export declare function excludeFuturePosts(futureSlugs: string[]): (url: string) => boolean;
|
|
46
|
+
/** Compose multiple filter functions with AND semantics. */
|
|
47
|
+
export declare function composeFilters(...filters: Array<(url: string) => boolean>): (url: string) => boolean;
|
|
48
|
+
/** Filter helper: exclude URLs matching any of the supplied patterns. */
|
|
49
|
+
export declare function excludePatterns(patterns: RegExp[]): (url: string) => boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the `@astrojs/sitemap` integration.
|
|
3
|
+
*
|
|
4
|
+
* Two complementary functions:
|
|
5
|
+
* - `defaultPriorityRules(site)` produces a `serialize` function that sets
|
|
6
|
+
* priority and changefreq based on URL shape (home, top-level hubs,
|
|
7
|
+
* service/area pages, blog posts).
|
|
8
|
+
* - `excludeFuturePosts(futureSlugs)` produces a `filter` function that
|
|
9
|
+
* drops scheduled blog posts whose slugs are in the supplied set.
|
|
10
|
+
*
|
|
11
|
+
* Compose both like:
|
|
12
|
+
*
|
|
13
|
+
* sitemap({
|
|
14
|
+
* filter: excludeFuturePosts(await blog.getFutureBlogSlugs()),
|
|
15
|
+
* serialize: defaultPriorityRules(SITE),
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returns a `serialize` function that assigns priority/changefreq based on
|
|
20
|
+
* URL shape. Tune via `options.rules` for site-specific top-level paths.
|
|
21
|
+
*
|
|
22
|
+
* @param site The site origin (no trailing slash), e.g. 'https://example.com'.
|
|
23
|
+
*/
|
|
24
|
+
export function defaultPriorityRules(site, options = {}) {
|
|
25
|
+
const homeUrl = `${site}/`;
|
|
26
|
+
const extra = options.rules ?? [];
|
|
27
|
+
return (item) => {
|
|
28
|
+
if (item.url === homeUrl) {
|
|
29
|
+
return { ...item, priority: 1.0, changefreq: 'daily' };
|
|
30
|
+
}
|
|
31
|
+
for (const rule of extra) {
|
|
32
|
+
if (rule.pattern.test(item.url)) {
|
|
33
|
+
return {
|
|
34
|
+
...item,
|
|
35
|
+
priority: rule.priority,
|
|
36
|
+
...(rule.changefreq && { changefreq: rule.changefreq }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Default heuristics.
|
|
41
|
+
if (/\/(services|service-areas|industries)\/$/.test(item.url)) {
|
|
42
|
+
return { ...item, priority: 0.9, changefreq: 'weekly' };
|
|
43
|
+
}
|
|
44
|
+
if (/\/(services|service-areas|industries)\/[^/]+\/$/.test(item.url)) {
|
|
45
|
+
return { ...item, priority: 0.8, changefreq: 'weekly' };
|
|
46
|
+
}
|
|
47
|
+
if (/\/blog\/$/.test(item.url)) {
|
|
48
|
+
return { ...item, priority: 0.7, changefreq: 'daily' };
|
|
49
|
+
}
|
|
50
|
+
if (/\/blog\/[^/]+\/$/.test(item.url)) {
|
|
51
|
+
return { ...item, priority: 0.6, changefreq: 'monthly' };
|
|
52
|
+
}
|
|
53
|
+
if (/\/blog\/(tag|category)\/[^/]+\/$/.test(item.url)) {
|
|
54
|
+
return { ...item, priority: 0.5, changefreq: 'weekly' };
|
|
55
|
+
}
|
|
56
|
+
return item;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns a `filter` function for `@astrojs/sitemap` that excludes any URL
|
|
61
|
+
* whose final path segment matches a future-dated blog slug.
|
|
62
|
+
*
|
|
63
|
+
* Pass `await blog.getFutureBlogSlugs()` to the constructor.
|
|
64
|
+
*/
|
|
65
|
+
export function excludeFuturePosts(futureSlugs) {
|
|
66
|
+
const set = new Set(futureSlugs);
|
|
67
|
+
return (url) => {
|
|
68
|
+
const m = url.match(/\/blog\/([^/]+)\/?$/);
|
|
69
|
+
if (!m)
|
|
70
|
+
return true;
|
|
71
|
+
return !set.has(m[1]);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Compose multiple filter functions with AND semantics. */
|
|
75
|
+
export function composeFilters(...filters) {
|
|
76
|
+
return (url) => filters.every((f) => f(url));
|
|
77
|
+
}
|
|
78
|
+
/** Filter helper: exclude URLs matching any of the supplied patterns. */
|
|
79
|
+
export function excludePatterns(patterns) {
|
|
80
|
+
return (url) => !patterns.some((p) => p.test(url));
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibalzam/codejitsu-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared core for Codejitsu Astro sites — reusable code and Claude-facing instructions for blog, SEO, images, deploy, and llms.txt.",
|
|
6
6
|
"keywords": [
|
|
@@ -31,14 +31,35 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"exports": {
|
|
34
|
-
".":
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"./
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./src/index.d.ts",
|
|
36
|
+
"default": "./src/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./config": {
|
|
39
|
+
"types": "./modules/config/src/index.d.ts",
|
|
40
|
+
"default": "./modules/config/src/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./blog": {
|
|
43
|
+
"types": "./modules/blog/src/index.d.ts",
|
|
44
|
+
"default": "./modules/blog/src/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./seo": {
|
|
47
|
+
"types": "./modules/seo/src/index.d.ts",
|
|
48
|
+
"default": "./modules/seo/src/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./seo/schema": {
|
|
51
|
+
"types": "./modules/seo/src/schema.d.ts",
|
|
52
|
+
"default": "./modules/seo/src/schema.js"
|
|
53
|
+
},
|
|
54
|
+
"./seo/sitemap": {
|
|
55
|
+
"types": "./modules/seo/src/sitemap.d.ts",
|
|
56
|
+
"default": "./modules/seo/src/sitemap.js"
|
|
57
|
+
},
|
|
58
|
+
"./seo/Head.astro": "./modules/seo/templates/Head.astro",
|
|
59
|
+
"./images": {
|
|
60
|
+
"types": "./modules/images/src/index.d.ts",
|
|
61
|
+
"default": "./modules/images/src/index.js"
|
|
62
|
+
},
|
|
42
63
|
"./llms": "./modules/llms/src/generate.mjs",
|
|
43
64
|
"./package.json": "./package.json"
|
|
44
65
|
},
|
|
@@ -58,7 +79,8 @@
|
|
|
58
79
|
],
|
|
59
80
|
"scripts": {
|
|
60
81
|
"typecheck": "tsc --noEmit",
|
|
61
|
-
"
|
|
82
|
+
"build": "tsc -p tsconfig.build.json",
|
|
83
|
+
"prepublishOnly": "npm run typecheck && npm run build"
|
|
62
84
|
},
|
|
63
85
|
"peerDependencies": {
|
|
64
86
|
"astro": ">=5.0.0"
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CORE_VERSION = "0.2.0";
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CORE_VERSION = '0.2.0';
|