diff --git a/examples/astro/astro.config.mjs b/examples/astro/astro.config.mjs
index 5035260e..b129cc63 100644
--- a/examples/astro/astro.config.mjs
+++ b/examples/astro/astro.config.mjs
@@ -1,8 +1,8 @@
-import galactiks, { integrationsPreset } from '@galactiks/astro-integration';
import path from 'path';
-
process.env.CONTENT_PATH = path.join(process.cwd(), '../contentlayer');
+import galactiks, { integrationsPreset } from '@galactiks/astro-integration';
+
// https://astro.build/config
export default /** @type {import('astro').AstroUserConfig} */ {
integrations: [integrationsPreset(), galactiks()],
diff --git a/examples/contentlayer/galactiks.config.json b/examples/contentlayer/galactiks.config.json
index 092a218b..1a47c868 100644
--- a/examples/contentlayer/galactiks.config.json
+++ b/examples/contentlayer/galactiks.config.json
@@ -4,6 +4,17 @@
"default": "en",
"available": ["en", "fr"]
},
+ "openGraph": {
+ "siteName": "Galactiks"
+ },
+ "facebook": {
+ "appId": "123456789"
+ },
+ "twitter": {
+ "handle": "@galactiks",
+ "site": "@galactiks",
+ "cardType": "summary_large_image"
+ },
"pages": {
"articles": {
"path": "/{+inLanguage}/a/{identifier}/"
diff --git a/packages/adapters/astro/src/components/Head.astro b/packages/adapters/astro/src/components/Head.astro
index c345b5ed..a2ac344a 100644
--- a/packages/adapters/astro/src/components/Head.astro
+++ b/packages/adapters/astro/src/components/Head.astro
@@ -30,6 +30,11 @@ const { analytics } = getConfig();
))
}
+ {
+ content.headers?.facebook?.map(({ property, content, ...props }) => (
+
+ ))
+ }
{
content.headers?.twitterCard?.map(({ name, content, ...props }) => (
diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts
index d1188a5d..130240df 100644
--- a/packages/config/src/config.ts
+++ b/packages/config/src/config.ts
@@ -6,6 +6,7 @@ import { z } from 'zod';
import { analyticsConfigSchema } from './analytics.js';
import { getDefaultConfig } from './default.js';
+import { facebookConfigSchema, openGraphConfigSchema, twitterConfigSchema } from './socials.js';
import type { WebManifest } from './webmanifest.config.js';
const localesSchema = z.object({
@@ -29,6 +30,9 @@ const galactiksConfigFileSchema = z.object({
locales: localesSchema.optional(),
template: z.string(),
analytics: analyticsConfigSchema.optional(),
+ openGraph: openGraphConfigSchema.optional(),
+ facebook: facebookConfigSchema.optional(),
+ twitter: twitterConfigSchema.optional(),
trailingSlash: z.enum(['ignore', 'always', 'never']).optional(),
pages: z
.object({
diff --git a/packages/config/src/socials.ts b/packages/config/src/socials.ts
new file mode 100644
index 00000000..9f14b6dd
--- /dev/null
+++ b/packages/config/src/socials.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+export const openGraphConfigSchema = z.object({
+ siteName: z.string().optional(),
+});
+
+export const facebookConfigSchema = z.object({
+ appId: z.string().optional(),
+});
+
+export const twitterConfigSchema = z.object({
+ creator: z.string().optional(),
+ site: z.string().optional(),
+});
diff --git a/packages/explorer/src/content/hydrate/missing-fields.ts b/packages/explorer/src/content/hydrate/missing-fields.ts
index 6014de2f..77c17ccd 100644
--- a/packages/explorer/src/content/hydrate/missing-fields.ts
+++ b/packages/explorer/src/content/hydrate/missing-fields.ts
@@ -13,6 +13,7 @@ import { breadcrumbBuilder } from '../metadata/breadcrumb.js';
import { getBasicHeaders } from '../metadata/headers.js';
import { getTwitterCard } from '../metadata/twitter.js';
import { getOpenGraphObjects } from '../metadata/ogp/og.js';
+import { getFacebookObjects } from '../metadata/facebook.js';
import { getStructuredDataSchemas } from '../metadata/schemas/structured-data.js';
export const computeMissingFields =
@@ -81,6 +82,7 @@ export const computeMissingFields =
contentWithoutHeaders
),
openGraph: getOpenGraphObjects(contentWithoutHeaders),
+ facebook: getFacebookObjects(),
twitterCard: getTwitterCard(contentWithoutHeaders),
},
};
diff --git a/packages/explorer/src/content/metadata/facebook.spec.ts b/packages/explorer/src/content/metadata/facebook.spec.ts
new file mode 100644
index 00000000..28e84d6c
--- /dev/null
+++ b/packages/explorer/src/content/metadata/facebook.spec.ts
@@ -0,0 +1,31 @@
+import { getFacebookObjects } from './facebook';
+import { getConfig } from '@galactiks/config';
+
+jest.mock('@galactiks/config');
+
+describe('getFacebookObjects', () => {
+ it('should return an empty array if facebook config is not present', () => {
+ (getConfig as jest.Mock).mockReturnValue({});
+
+ const result = getFacebookObjects();
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return an array with fb:app_id if facebook appId is present', () => {
+ const mockAppId = '1234567890';
+ (getConfig as jest.Mock).mockReturnValue({ facebook: { appId: mockAppId } });
+
+ const result = getFacebookObjects();
+
+ expect(result).toEqual([{ property: 'fb:app_id', content: mockAppId }]);
+ });
+
+ it('should return an empty array if facebook config is present but appId is not', () => {
+ (getConfig as jest.Mock).mockReturnValue({ facebook: {} });
+
+ const result = getFacebookObjects();
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/explorer/src/content/metadata/facebook.ts b/packages/explorer/src/content/metadata/facebook.ts
new file mode 100644
index 00000000..b402adea
--- /dev/null
+++ b/packages/explorer/src/content/metadata/facebook.ts
@@ -0,0 +1,12 @@
+import { getConfig } from '@galactiks/config';
+import type { MetadataHeaders } from '../../types/index.js';
+
+export const getFacebookObjects = (): MetadataHeaders['facebook'] => {
+ const { facebook } = getConfig();
+ const headers = [];
+ if (facebook?.appId) {
+ headers.push({ property: 'fb:app_id', content: facebook.appId });
+ }
+
+ return headers;
+};
diff --git a/packages/explorer/src/content/metadata/headers.spec.ts b/packages/explorer/src/content/metadata/headers.spec.ts
index 1603d4a3..b5653581 100644
--- a/packages/explorer/src/content/metadata/headers.spec.ts
+++ b/packages/explorer/src/content/metadata/headers.spec.ts
@@ -1,7 +1,14 @@
+import { getConfig } from '@galactiks/config';
import { Content } from '../../types/index.js';
import { getBasicHeaders } from './headers';
+jest.mock('@galactiks/config');
+
describe('getBasicHeaders', () => {
+ beforeAll(() => {
+ (getConfig as jest.Mock).mockReturnValue({});
+ });
+
it('should return correct basic headers', () => {
const entry = {
name: 'Test Document',
diff --git a/packages/explorer/src/content/metadata/ogp/og.ts b/packages/explorer/src/content/metadata/ogp/og.ts
index 1075b840..dc0a4e3b 100644
--- a/packages/explorer/src/content/metadata/ogp/og.ts
+++ b/packages/explorer/src/content/metadata/ogp/og.ts
@@ -1,14 +1,27 @@
+import { getConfig } from '@galactiks/config';
import type { Content, MetadataHeaders } from '../../../types/index.js';
import { getArticle } from './article.js';
import { getWebsite } from './website.js';
-export const getOpenGraphObjects = (
- document: Content
-): MetadataHeaders['openGraph'] => {
+const getOpenGraphByType = (document: Content): MetadataHeaders['openGraph'] => {
if (document.type === 'Article') {
return getArticle(document);
}
return getWebsite(document);
+}
+
+export const getOpenGraphObjects = (
+ document: Content
+): MetadataHeaders['openGraph'] => {
+ const headers = getOpenGraphByType(document) || [];
+
+ const { webManifest, openGraph } = getConfig();
+ const siteName = openGraph?.siteName || webManifest?.name;
+ if (siteName) {
+ headers.push({ property: 'og:siteName', content: siteName });
+ }
+
+ return headers;
};
diff --git a/packages/explorer/src/content/metadata/twitter.spec.ts b/packages/explorer/src/content/metadata/twitter.spec.ts
index 8eb74550..87df26c9 100644
--- a/packages/explorer/src/content/metadata/twitter.spec.ts
+++ b/packages/explorer/src/content/metadata/twitter.spec.ts
@@ -1,7 +1,14 @@
+import { getConfig } from '@galactiks/config';
import { Content } from '../../types/index.js';
import { getTwitterCard } from './twitter';
+jest.mock('@galactiks/config');
+
describe('getTwitterCard', () => {
+ beforeAll(() => {
+ (getConfig as jest.Mock).mockReturnValue({});
+ });
+
it('should return correct headers when document has an image', () => {
const document = {
name: 'Test Document',
@@ -37,4 +44,72 @@ describe('getTwitterCard', () => {
{ name: 'twitter:card', content: 'summary' },
]);
});
+
+ it('should include twitter:site if configured', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ twitter: {
+ site: '@testsite',
+ },
+ });
+
+ const document = {
+ name: 'Test Document',
+ description: 'Test Description',
+ } as Content;
+
+ const result = getTwitterCard(document);
+
+ expect(result).toEqual([
+ { name: 'twitter:title', content: 'Test Document' },
+ { name: 'twitter:description', content: 'Test Description' },
+ { name: 'twitter:site', content: '@testsite' },
+ { name: 'twitter:card', content: 'summary' },
+ ]);
+ });
+
+ it('should include twitter:creator if configured', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ twitter: {
+ creator: '@testcreator',
+ },
+ });
+
+ const document = {
+ name: 'Test Document',
+ description: 'Test Description',
+ } as Content;
+
+ const result = getTwitterCard(document);
+
+ expect(result).toEqual([
+ { name: 'twitter:title', content: 'Test Document' },
+ { name: 'twitter:description', content: 'Test Description' },
+ { name: 'twitter:creator', content: '@testcreator' },
+ { name: 'twitter:card', content: 'summary' },
+ ]);
+ });
+
+ it('should include both twitter:site and twitter:creator if configured', () => {
+ (getConfig as jest.Mock).mockReturnValue({
+ twitter: {
+ creator: '@testcreator',
+ site: '@testsite',
+ },
+ });
+
+ const document = {
+ name: 'Test Document',
+ description: 'Test Description',
+ } as Content;
+
+ const result = getTwitterCard(document);
+
+ expect(result).toEqual([
+ { name: 'twitter:title', content: 'Test Document' },
+ { name: 'twitter:description', content: 'Test Description' },
+ { name: 'twitter:site', content: '@testsite' },
+ { name: 'twitter:creator', content: '@testcreator' },
+ { name: 'twitter:card', content: 'summary' },
+ ]);
+ });
});
diff --git a/packages/explorer/src/content/metadata/twitter.ts b/packages/explorer/src/content/metadata/twitter.ts
index 25717395..18dd0b6c 100644
--- a/packages/explorer/src/content/metadata/twitter.ts
+++ b/packages/explorer/src/content/metadata/twitter.ts
@@ -1,13 +1,23 @@
+import { getConfig } from '@galactiks/config';
import type { Content, MetadataHeaders } from '../../types/index.js';
export const getTwitterCard = (
document: Content
): MetadataHeaders['twitterCard'] => {
+ const { twitter } = getConfig();
+
const headers = [
{ name: 'twitter:title', content: document.name },
{ name: 'twitter:description', content: document.description },
];
+ if (twitter?.site) {
+ headers.push({ name: 'twitter:site', content: twitter.site });
+ }
+ if (twitter?.creator) {
+ headers.push({ name: 'twitter:creator', content: twitter.creator });
+ }
+
if (document.image) {
headers.push(
{ name: 'twitter:card', content: 'summary_large_image' },
diff --git a/packages/explorer/src/types/_schemas.ts b/packages/explorer/src/types/_schemas.ts
index 3b7b7671..65f05df6 100644
--- a/packages/explorer/src/types/_schemas.ts
+++ b/packages/explorer/src/types/_schemas.ts
@@ -24,6 +24,14 @@ const metadataHeaders = z
})
)
.optional(),
+ facebook: z
+ .array(
+ z.object({
+ property: z.string(),
+ content: z.string(),
+ })
+ )
+ .optional(),
twitterCard: z
.array(
z.object({
diff --git a/turbo.json b/turbo.json
index 6067656b..95070323 100644
--- a/turbo.json
+++ b/turbo.json
@@ -29,6 +29,7 @@
"cache": false
},
"test": {
+ "dependsOn": ["^build"],
"outputLogs": "new-only"
}
}