Skip to content

Commit d81d1cc

Browse files
authored
fix(astro): better default meta tags (#342)
1 parent 3e30953 commit d81d1cc

File tree

14 files changed

+133
-31
lines changed

14 files changed

+133
-31
lines changed

docs/tutorialkit.dev/src/content/docs/guides/deployment.mdx

+11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ This will generate a `dist` directory containing the static files that make up y
3434

3535
You can learn more about the build process in the [Astro documentation](https://docs.astro.build/en/reference/cli-reference/#astro-build).
3636

37+
## Environment variables
38+
39+
The [`site`](https://docs.astro.build/reference/configuration-reference/#site) configuration should point to your website's absolute URL.
40+
This will allow to compute absolute URLs for SEO metadata.
41+
42+
Example:
43+
```js
44+
// astro.config.mjs
45+
site:"https://tutorialkit.dev"
46+
```
47+
3748
## Headers configuration
3849

3950
The preview and terminal features in TutorialKit rely on WebContainers technology. To ensure that this technology works correctly, you need to configure the headers of your web server to ensure the site is cross-origin isolated (you can read more about this at [webcontainers.io](https://webcontainers.io/guides/configuring-headers)).

packages/astro/src/default/components/Logo.astro

+2-21
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,11 @@
11
---
2-
import fs from 'node:fs';
3-
import path from 'node:path';
4-
import { joinPaths } from '../utils/url';
5-
6-
const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];
2+
import { LOGO_EXTENSIONS } from '../utils/constants';
3+
import { readLogoFile } from '../utils/logo';
74
85
interface Props {
96
logoLink: string;
107
}
118
12-
function readLogoFile(logoPrefix: string) {
13-
let logo;
14-
15-
for (const logoExt of LOGO_EXTENSIONS) {
16-
const logoFilename = `${logoPrefix}.${logoExt}`;
17-
const exists = fs.existsSync(path.join('public', logoFilename));
18-
19-
if (exists) {
20-
logo = joinPaths(import.meta.env.BASE_URL, logoFilename);
21-
break;
22-
}
23-
}
24-
25-
return logo;
26-
}
27-
289
const { logoLink } = Astro.props;
2910
3011
const logo = readLogoFile('logo');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
import type { MetaTagsConfig } from '@tutorialkit/types';
3+
import { readLogoFile } from '../utils/logo';
4+
import { readPublicAsset } from '../utils/publicAsset';
5+
6+
interface Props {
7+
meta?: MetaTagsConfig;
8+
}
9+
const { meta = {} } = Astro.props;
10+
let imageUrl;
11+
if (meta.image) {
12+
imageUrl = readPublicAsset(meta.image, true);
13+
if (!imageUrl) {
14+
console.warn(`Image ${meta.image} not found in "/public" folder`);
15+
}
16+
}
17+
imageUrl ??= readLogoFile('logo', true);
18+
---
19+
20+
<meta charset="UTF-8" />
21+
<meta name="viewport" content="width=device-width" />
22+
<meta name="generator" content={Astro.generator} />
23+
{meta.description ? <meta name="description" content={meta.description} /> : null}
24+
{/* open graph */}
25+
{meta.title ? <meta name="og:title" content={meta.title} /> : null}
26+
{meta.description ? <meta name="og:description" content={meta.description} /> : null}
27+
{imageUrl ? <meta name="og:image" content={imageUrl} /> : null}
28+
{/* twitter */}
29+
{meta.title ? <meta name="twitter:title" content={meta.title} /> : null}
30+
{meta.description ? <meta name="twitter:description" content={meta.description} /> : null}
31+
{imageUrl ? <meta name="twitter:image" content={imageUrl} /> : null}

packages/astro/src/default/layouts/Layout.astro

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
---
22
import { ViewTransitions } from 'astro:transitions';
3-
import { joinPaths } from '../utils/url';
3+
import type { MetaTagsConfig } from '@tutorialkit/types';
4+
import MetaTags from '../components/MetaTags.astro';
5+
import { readPublicAsset } from '../utils/publicAsset';
46
57
interface Props {
68
title: string;
9+
meta?: MetaTagsConfig;
710
}
8-
9-
const { title } = Astro.props;
10-
const baseURL = import.meta.env.BASE_URL;
11+
const { title, meta } = Astro.props;
12+
const faviconUrl = readPublicAsset('favicon.svg');
1113
---
1214

1315
<!doctype html>
1416
<html lang="en" transition:animate="none" class="h-full overflow-hidden">
1517
<head>
16-
<meta charset="UTF-8" />
17-
<meta name="description" content="TutorialKit" />
18-
<meta name="viewport" content="width=device-width" />
19-
<meta name="generator" content={Astro.generator} />
2018
<title>{title}</title>
21-
<link rel="icon" type="image/svg+xml" href={joinPaths(baseURL, '/favicon.svg')} />
19+
{faviconUrl ? <link rel="icon" type="image/svg+xml" href={faviconUrl} /> : null}
20+
<MetaTags meta={meta} />
2221
<link rel="preconnect" href="https://fonts.googleapis.com" />
2322
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2423
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

packages/astro/src/default/pages/[...slug].astro

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ export async function getStaticPaths() {
1515
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
1616
1717
const { lesson, logoLink, navList, title } = Astro.props as Props;
18+
const meta = lesson.data?.meta ?? {};
19+
20+
// use lesson's default title and a default description for SEO metadata
21+
meta.title ??= title;
22+
meta.description ??= 'A TutorialKit interactive lesson';
1823
---
1924

20-
<Layout title={title}>
25+
<Layout title={title} meta={meta}>
2126
<PageLoadingIndicator />
2227
<div id="previews-container"></div>
2328
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>

packages/astro/src/default/utils/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export const RESIZABLE_PANELS = {
22
Main: 'main',
33
} as const;
44
export const IGNORED_FILES = ['**/.DS_Store', '**/*.swp'];
5+
6+
export const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];

packages/astro/src/default/utils/content.ts

+1
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ export async function getTutorial(): Promise<Tutorial> {
245245
'editor',
246246
'focus',
247247
'i18n',
248+
'meta',
248249
'editPageLink',
249250
'openInStackBlitz',
250251
'filesystem',
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { LOGO_EXTENSIONS } from './constants';
2+
import { readPublicAsset } from './publicAsset';
3+
4+
export function readLogoFile(logoPrefix: string = 'logo', absolute?: boolean) {
5+
let logo;
6+
7+
for (const logoExt of LOGO_EXTENSIONS) {
8+
const logoFilename = `${logoPrefix}.${logoExt}`;
9+
logo = readPublicAsset(logoFilename, absolute);
10+
11+
if (logo) {
12+
break;
13+
}
14+
}
15+
16+
return logo;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { joinPaths } from './url';
4+
5+
export function readPublicAsset(filename: string, absolute?: boolean) {
6+
let asset;
7+
const exists = fs.existsSync(path.join('public', filename));
8+
9+
if (!exists) {
10+
return;
11+
}
12+
13+
asset = joinPaths(import.meta.env.BASE_URL, filename);
14+
15+
if (absolute) {
16+
const site = import.meta.env.SITE;
17+
18+
if (!site) {
19+
// the SITE env variable inherits the value from Astro.site configuration
20+
console.warn('Trying to compute an absolute file URL but Astro.site is not set.');
21+
} else {
22+
asset = joinPaths(site, asset);
23+
}
24+
}
25+
26+
return asset;
27+
}

packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap

+3
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ exports[`create and eject a project 1`] = `
206206
"src/components/LoginButton.tsx",
207207
"src/components/Logo.astro",
208208
"src/components/MainContainer.astro",
209+
"src/components/MetaTags.astro",
209210
"src/components/MobileContentToggle.astro",
210211
"src/components/NavCard.astro",
211212
"src/components/NavWrapper.tsx",
@@ -307,7 +308,9 @@ exports[`create and eject a project 1`] = `
307308
"src/utils/content/files-ref.ts",
308309
"src/utils/content/squash.ts",
309310
"src/utils/logger.ts",
311+
"src/utils/logo.ts",
310312
"src/utils/nav.ts",
313+
"src/utils/publicAsset.ts",
311314
"src/utils/routes.ts",
312315
"src/utils/url.ts",
313316
"src/utils/workspace.ts",

packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/content.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ prepareCommands:
1111
- ['node -e setTimeout(()=>{process.exit(1)},5000)', 'This is going to fail']
1212
terminal:
1313
panels: ['terminal', 'output']
14+
meta:
15+
description: "This is lesson 1"
16+
image: "/logo.svg"
1417
---
1518

1619
# Kitchen Sink [Heading 1]

packages/types/src/entities/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { I18nSchema } from '../schemas/i18n.js';
22
import type { ChapterSchema, LessonSchema, PartSchema } from '../schemas/index.js';
3+
import type { MetaTagsSchema } from '../schemas/metatags.js';
34

45
export type * from './nav.js';
56

@@ -57,6 +58,8 @@ export interface Lesson<T = unknown> {
5758

5859
export type I18n = Required<NonNullable<I18nSchema>>;
5960

61+
export type MetaTagsConfig = MetaTagsSchema;
62+
6063
export interface Tutorial {
6164
logoLink?: string;
6265
firstPartId?: string;

packages/types/src/schemas/common.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import { i18nSchema } from './i18n.js';
3+
import { metaTagsSchema } from './metatags.js';
34

45
export const commandSchema = z.union([
56
// a single string, the command to run
@@ -207,6 +208,8 @@ export type TerminalSchema = z.infer<typeof terminalSchema>;
207208
export type EditorSchema = z.infer<typeof editorSchema>;
208209

209210
export const webcontainerSchema = commandsSchema.extend({
211+
meta: metaTagsSchema.optional(),
212+
210213
previews: previewSchema
211214
.optional()
212215
.describe(
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { z } from 'zod';
2+
3+
export const metaTagsSchema = z.object({
4+
image: z
5+
.string()
6+
.optional()
7+
/**
8+
* Ideally we would want to use `image` from:
9+
* https://docs.astro.build/en/guides/images/#images-in-content-collections .
10+
*/
11+
.describe('A relative path to an image that lives in the public folder to show on social previews.'),
12+
description: z.string().optional().describe('A description for metadata'),
13+
title: z.string().optional().describe('A title to use specifically for metadata'),
14+
});
15+
16+
export type MetaTagsSchema = z.infer<typeof metaTagsSchema>;

0 commit comments

Comments
 (0)