Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(astro): better default meta tags #342

Merged
merged 14 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/tutorialkit.dev/src/content/docs/guides/deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ This will generate a `dist` directory containing the static files that make up y

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

## Environment variables

The [`site`](https://docs.astro.build/reference/configuration-reference/#site) configuration should point to your website's absolute URL.
This will allow to compute absolute URLs for SEO metadata.

Example:
```js
// astro.config.mjs
site:"https://tutorialkit.dev"
```

## Headers configuration

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)).
Expand Down
23 changes: 2 additions & 21 deletions packages/astro/src/default/components/Logo.astro
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
---
import fs from 'node:fs';
import path from 'node:path';
import { joinPaths } from '../utils/url';

const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];
import { LOGO_EXTENSIONS } from '../utils/constants';
import { readLogoFile } from '../utils/logo';

interface Props {
logoLink: string;
}

function readLogoFile(logoPrefix: string) {
let logo;

for (const logoExt of LOGO_EXTENSIONS) {
const logoFilename = `${logoPrefix}.${logoExt}`;
const exists = fs.existsSync(path.join('public', logoFilename));

if (exists) {
logo = joinPaths(import.meta.env.BASE_URL, logoFilename);
break;
}
}

return logo;
}

const { logoLink } = Astro.props;

const logo = readLogoFile('logo');
Expand Down
31 changes: 31 additions & 0 deletions packages/astro/src/default/components/MetaTags.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
import type { MetaTagsConfig } from '@tutorialkit/types';
import { readLogoFile } from '../utils/logo';
import { readPublicAsset } from '../utils/publicAsset';

interface Props {
meta?: MetaTagsConfig;
}
const { meta = {} } = Astro.props;
let imageUrl;
if (meta.image) {
imageUrl = readPublicAsset(meta.image, true);
if (!imageUrl) {
console.warn(`Image ${meta.image} not found in "/public" folder`);
}
}
imageUrl ??= readLogoFile('logo', true);
---

<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
{meta.description ? <meta name="description" content={meta.description} /> : null}
{/* open graph */}
{meta.title ? <meta name="og:title" content={meta.title} /> : null}
{meta.description ? <meta name="og:description" content={meta.description} /> : null}
{imageUrl ? <meta name="og:image" content={imageUrl} /> : null}
{/* twitter */}
{meta.title ? <meta name="twitter:title" content={meta.title} /> : null}
{meta.description ? <meta name="twitter:description" content={meta.description} /> : null}
{imageUrl ? <meta name="twitter:image" content={imageUrl} /> : null}
17 changes: 8 additions & 9 deletions packages/astro/src/default/layouts/Layout.astro
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
---
import { ViewTransitions } from 'astro:transitions';
import { joinPaths } from '../utils/url';
import type { MetaTagsConfig } from '@tutorialkit/types';
import MetaTags from '../components/MetaTags.astro';
import { readPublicAsset } from '../utils/publicAsset';

interface Props {
title: string;
eric-burel marked this conversation as resolved.
Show resolved Hide resolved
meta?: MetaTagsConfig;
}

const { title } = Astro.props;
const baseURL = import.meta.env.BASE_URL;
const { title, meta } = Astro.props;
const faviconUrl = readPublicAsset('favicon.svg');
---

<!doctype html>
<html lang="en" transition:animate="none" class="h-full overflow-hidden">
<head>
<meta charset="UTF-8" />
<meta name="description" content="TutorialKit" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href={joinPaths(baseURL, '/favicon.svg')} />
{faviconUrl ? <link rel="icon" type="image/svg+xml" href={faviconUrl} /> : null}
<MetaTags meta={meta} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/default/pages/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ export async function getStaticPaths() {
type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { lesson, logoLink, navList, title } = Astro.props as Props;
const meta = lesson.data?.meta ?? {};

// use lesson's default title and a default description for SEO metadata
meta.title ??= title;
meta.description ??= 'A TutorialKit interactive lesson';
---

<Layout title={title}>
<Layout title={title} meta={meta}>
<PageLoadingIndicator />
<div id="previews-container"></div>
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/default/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const RESIZABLE_PANELS = {
Main: 'main',
} as const;
export const IGNORED_FILES = ['**/.DS_Store', '**/*.swp'];

export const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];
1 change: 1 addition & 0 deletions packages/astro/src/default/utils/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export async function getTutorial(): Promise<Tutorial> {
'editor',
'focus',
'i18n',
'meta',
AriPerkkio marked this conversation as resolved.
Show resolved Hide resolved
'editPageLink',
'openInStackBlitz',
'filesystem',
Expand Down
17 changes: 17 additions & 0 deletions packages/astro/src/default/utils/logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { LOGO_EXTENSIONS } from './constants';
import { readPublicAsset } from './publicAsset';

export function readLogoFile(logoPrefix: string = 'logo', absolute?: boolean) {
let logo;

for (const logoExt of LOGO_EXTENSIONS) {
const logoFilename = `${logoPrefix}.${logoExt}`;
logo = readPublicAsset(logoFilename, absolute);

if (logo) {
break;
}
}

return logo;
}
27 changes: 27 additions & 0 deletions packages/astro/src/default/utils/publicAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from 'node:fs';
import path from 'node:path';
import { joinPaths } from './url';

export function readPublicAsset(filename: string, absolute?: boolean) {
let asset;
const exists = fs.existsSync(path.join('public', filename));

if (!exists) {
return;
}

asset = joinPaths(import.meta.env.BASE_URL, filename);

if (absolute) {
const site = import.meta.env.SITE;

if (!site) {
// the SITE env variable inherits the value from Astro.site configuration
console.warn('Trying to compute an absolute file URL but Astro.site is not set.');
} else {
asset = joinPaths(site, asset);
}
}

return asset;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ prepareCommands:
- ['node -e setTimeout(()=>{process.exit(1)},5000)', 'This is going to fail']
terminal:
panels: ['terminal', 'output']
meta:
description: "This is lesson 1"
image: "/logo.svg"
---

# Kitchen Sink [Heading 1]
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { I18nSchema } from '../schemas/i18n.js';
import type { ChapterSchema, LessonSchema, PartSchema } from '../schemas/index.js';
import type { MetaTagsSchema } from '../schemas/metatags.js';

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

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

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

export type MetaTagsConfig = MetaTagsSchema;

export interface Tutorial {
logoLink?: string;
firstPartId?: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/schemas/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { i18nSchema } from './i18n.js';
import { metaTagsSchema } from './metatags.js';

export const commandSchema = z.union([
// a single string, the command to run
Expand Down Expand Up @@ -286,6 +287,7 @@ export const webcontainerSchema = commandsSchema.extend({

export const baseSchema = webcontainerSchema.extend({
title: z.string().describe('The title of the part, chapter, or lesson.'),
meta: metaTagsSchema.optional(),
slug: z
.string()
.optional()
Expand Down
16 changes: 16 additions & 0 deletions packages/types/src/schemas/metatags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod';

export const metaTagsSchema = z.object({
image: z
.string()
.optional()
/**
* Ideally we would want to use `image` from:
* https://docs.astro.build/en/guides/images/#images-in-content-collections .
*/
.describe('A relative path to an image that lives in the public folder to show on social previews.'),
description: z.string().optional().describe('A description for metadata'),
title: z.string().optional().describe('A title to use specifically for metadata'),
});

export type MetaTagsSchema = z.infer<typeof metaTagsSchema>;
Loading