Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
feat: External link icon + open in new tab (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
LekoArts authored Feb 17, 2024
1 parent c73a0b8 commit 200cfa0
Show file tree
Hide file tree
Showing 14 changed files with 682 additions and 379 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ declare global {

## 404 Error Page Tracking

Besides adding the custom event to the `src/pages/404.jsx` page you'll also need to create a custom event goal in Plausible. See [their documentation](https://plausible.io/docs/404-error-pages-tracking) to learn more.
Besides adding the custom event to the `src/pages/404.jsx` page you'll also need to create a custom event goal in Plausible. See [their documentation](https://plausible.io/docs/error-pages-tracking-404) to learn more.

Here's an example of a 404 Page written in TypeScript:

Expand Down
5 changes: 0 additions & 5 deletions gatsby-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ const gatsbyConfig = {
},
},
`gatsby-plugin-sharp`,
`gatsby-plugin-catch-links`,
`gatsby-plugin-vanilla-extract`,
`gatsby-plugin-image`,
// Overwrite the default "slugify" option
Expand Down Expand Up @@ -261,10 +260,6 @@ const gatsbyConfig = {
],
},
},
{
resolve: `gatsby-plugin-gatsby-cloud`,
options: {},
},
].filter(Boolean),
}

Expand Down
10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,14 @@
"@vanilla-extract/webpack-plugin": "^2.3.1",
"gatsby": "^5.12.12",
"gatsby-adapter-netlify": "^1.0.4",
"gatsby-plugin-catch-links": "^5.12.0",
"gatsby-plugin-feed": "^5.12.3",
"gatsby-plugin-gatsby-cloud": "^5.12.2",
"gatsby-plugin-image": "^3.12.3",
"gatsby-plugin-manifest": "^5.12.3",
"gatsby-plugin-mdx": "^5.12.3",
"gatsby-plugin-perf-budgets": "^0.0.18",
"gatsby-plugin-sharp": "^5.12.3",
"gatsby-plugin-sitemap": "^6.12.3",
"gatsby-plugin-vanilla-extract": "^4.0.1",
"gatsby-plugin-webpack-bundle-analyser-v2": "^1.1.32",
"gatsby-remark-images": "^7.12.3",
"gatsby-source-filesystem": "^5.12.1",
"gatsby-source-graphql": "^5.12.1",
Expand All @@ -75,7 +72,7 @@
},
"devDependencies": {
"@netlify/edge-functions": "^2.2.0",
"@playwright/test": "^1.40.1",
"@playwright/test": "^1.41.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@types/lodash": "^4.14.202",
Expand All @@ -84,6 +81,7 @@
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"@vanilla-extract/vite-plugin": "^4.0.4",
"cross-env": "^7.0.3",
"cspell": "^7.3.9",
"eslint": "^8.55.0",
Expand All @@ -96,11 +94,11 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"jsdom": "^22.1.0",
"jsdom": "^24.0.0",
"lint-staged": "^14.0.1",
"prettier": "^3.1.1",
"typescript": "^5.3.3",
"vitest": "^1.0.4"
"vitest": "^1.3.0"
},
"packageManager": "[email protected]"
}
9 changes: 5 additions & 4 deletions playwright/meta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ const metaTagAssertions = [
{
name: `Blog Post (Tutorial)`,
url: `/react/using-deferred-static-generation-with-analytics-tools/`,
title: `Using Deferred Static Generation with Analytics Tools | ${site.title}`,
title: `Using Deferred Static Generation With Analytics Tools | ${site.title}`,
metaTags: [
{
key: `og:title`,
value: `Using Deferred Static Generation with Analytics Tools`,
value: `Using Deferred Static Generation With Analytics Tools | ${site.title}`,
},
{
key: `og:description`,
Expand Down Expand Up @@ -59,7 +59,7 @@ const metaTagAssertions = [
metaTags: [
{
key: `og:title`,
value: `Introducing the Theme UI Plugin for Figma`,
value: `Introducing the Theme UI Plugin for Figma | ${site.title}`,
},
{
key: `og:description`,
Expand Down Expand Up @@ -88,7 +88,7 @@ const metaTagAssertions = [
metaTags: [
{
key: `og:title`,
value: `How to Add Plausible Analytics to Gatsby`,
value: `How to Add Plausible Analytics to Gatsby | ${site.title}`,
},
{
key: `og:description`,
Expand Down Expand Up @@ -137,6 +137,7 @@ test.describe(`Meta Tags`, () => {
for (const tag of assertion.metaTags) {
let content: string | null

// @ts-ignore
if (tag.type === `name`) {
content = await page.locator(`meta[name="${tag.key}"]`).getAttribute(`value`)
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/arrow-up-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions src/components/mdx/__tests__/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @vitest-environment jsdom
*/

import * as React from "react"
import { render, screen } from "@testing-library/react"
import { MarkdownLink } from "../link"

describe(`MarkdownLink`, () => {
it(`should render internal link`, () => {
render(<MarkdownLink href="/internal">Internal Link</MarkdownLink>)
const link = screen.getByRole(`link`)
expect(link).toHaveAttribute(`data-link-internal`)
})

it(`should render external link`, () => {
render(<MarkdownLink href="https://example.com">External Link</MarkdownLink>)
const link = screen.getByRole(`link`)
expect(link).toHaveAttribute(`data-link-external`)
expect(link).toHaveAttribute(`target`, `_blank`)
expect(link).toHaveAttribute(`rel`, `noopener noreferrer`)
expect(link).toHaveTextContent(`(opens in a new tab)`)
})

it(`should render hash link`, () => {
render(<MarkdownLink href="#hash">Hash Link</MarkdownLink>)
const link = screen.getByRole(`link`)
expect(link).toHaveAttribute(`href`, `#hash`)
expect(link).not.toHaveAttribute(`data-link-internal`)
expect(link).not.toHaveAttribute(`data-link-external`)
})

it(`should render mailto link`, () => {
render(<MarkdownLink href="mailto:[email protected]">Mailto Link</MarkdownLink>)
const link = screen.getByRole(`link`)
expect(link).toHaveAttribute(`href`, `mailto:[email protected]`)
expect(link).not.toHaveAttribute(`data-link-internal`)
expect(link).not.toHaveAttribute(`data-link-external`)
})
})
3 changes: 3 additions & 0 deletions src/components/mdx/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Video } from "./video"
import { YouTube } from "./youtube"
import { Playground } from "./playground"
import { preToCodeBlock } from "../../utils/code"
import { MarkdownLink } from "./link"

// @ts-ignore
export const components: Components = {
Expand All @@ -21,6 +22,8 @@ export const components: Components = {
// it's possible to have a pre without a code in it
return <pre {...preProps} />
},
// @ts-ignore
a: (props) => <MarkdownLink {...props} />,
Alert,
Collapsible,
Video,
Expand Down
45 changes: 45 additions & 0 deletions src/components/mdx/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from "react"
import { Link as GatsbyLink } from "gatsby"
import { isInternalUrl } from "../../utils/is-internal-url"
import { VisuallyHidden } from "../a11y/visually-hidden"

/**
* Use Gatsby's link component for internal links.
* Set target="_blank" for external links and add data attribute for CSS styling.
*/
export const MarkdownLink = ({ href, children, ...rest }) => {
// If URL is a hash link, use anchor tag
if (href.startsWith(`#`)) {
return (
<a href={href} {...rest}>
{children}
</a>
)
}

// If internal, use Gatsby's link component
if (isInternalUrl(href)) {
return (
<GatsbyLink data-link-internal to={href} {...rest}>
{children}
</GatsbyLink>
)
}

// If URL is a protocol like mailto or tel, use anchor tag
if (!href.startsWith(`http`)) {
return (
<a href={href} {...rest}>
{children}
</a>
)
}

// At this point the link can only be external, style as such
return (
<a data-link-external target="_blank" rel="noopener noreferrer" href={href} {...rest}>
{children}
<VisuallyHidden> (opens in a new tab)</VisuallyHidden>
</a>
)
}
22 changes: 22 additions & 0 deletions src/components/typography/tailwind-typography.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { fonts } from "../../styles/fonts.css"
import { colorPalette } from "../../styles/tokens/colors"
import type { SelectorMap } from "../../utils/vanilla-extract"

// @ts-ignore
import arrowRightUp from "../../assets/icons/arrow-up-right.svg"

const nullHelper = null as unknown as string

export const proseRootMobile = {
Expand Down Expand Up @@ -195,6 +198,9 @@ export const proseSmVariant = {
"tbody td:last-of-type": {
paddingRight: vars.space[0] as string,
},
"a[data-link-external]::after": {
paddingRight: vars.space[5] as string,
},
}

export const proseMdVariant: typeof proseSmVariant = {
Expand Down Expand Up @@ -365,6 +371,9 @@ export const proseMdVariant: typeof proseSmVariant = {
"tbody td:last-of-type": {
paddingRight: nullHelper,
},
"a[data-link-external]::after": {
paddingRight: nullHelper,
},
}

export const proseLgVariant: typeof proseSmVariant = {
Expand Down Expand Up @@ -535,6 +544,9 @@ export const proseLgVariant: typeof proseSmVariant = {
"tbody td:last-of-type": {
paddingRight: vars.space[0],
},
"a[data-link-external]::after": {
paddingRight: nullHelper,
},
}

export const proseXlVariant: typeof proseSmVariant = {
Expand Down Expand Up @@ -705,6 +717,9 @@ export const proseXlVariant: typeof proseSmVariant = {
"tbody td:last-of-type": {
paddingRight: vars.space[0],
},
"a[data-link-external]::after": {
paddingRight: vars.space[6],
},
}

export const proseBaseStyle: SelectorMap = {
Expand All @@ -717,6 +732,13 @@ export const proseBaseStyle: SelectorMap = {
"a:hover": {
textDecoration: `none`,
},
"a[data-link-external]::after": {
content: ``,
backgroundImage: `url(${arrowRightUp})`,
backgroundPositionX: `50%`,
backgroundPositionY: `center`,
backgroundRepeat: `no-repeat`,
},
strong: {
color: {
light: colorPalette.gray[900],
Expand Down
20 changes: 20 additions & 0 deletions src/utils/__tests__/is-internal-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { isInternalUrl } from "../is-internal-url"

describe(`isInternalUrl`, () => {
it(`returns true for internal URLs`, () => {
expect(isInternalUrl(`/`)).toBe(true)
expect(isInternalUrl(`/about/`)).toBe(true)
expect(isInternalUrl(`/about/#anchor`)).toBe(true)
expect(isInternalUrl(`https://www.lekoarts.de`)).toBe(true)
expect(isInternalUrl(`https://www.lekoarts.de/about/`)).toBe(true)
expect(isInternalUrl(`https://www.lekoarts.de/about/#anchor`)).toBe(true)
})

it(`returns false for external URLs`, () => {
expect(isInternalUrl(`https://example.com`)).toBe(false)
expect(isInternalUrl(`https://example.com/about`)).toBe(false)
expect(isInternalUrl(`https://example.com/about#anchor`)).toBe(false)
expect(isInternalUrl(`https://example.com/about/`)).toBe(false)
expect(isInternalUrl(`https://example.com/about/#anchor`)).toBe(false)
})
})
7 changes: 7 additions & 0 deletions src/utils/is-internal-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { site } from "../constants/meta.mjs"

const base = new URL(site.url)

export function isInternalUrl(url: string): boolean {
return new URL(url, base).hostname === base.hostname
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"include": [
"packages/**/*.ts",
"src/**/__tests__/*.tsx",
"src/**/*.ts",
".eslintrc.js",
"lint-staged.config.js",
Expand All @@ -22,6 +23,6 @@
"skipLibCheck": true,
"noImplicitAny": false,
"resolveJsonModule": true,
"types": ["vitest/globals"]
"types": ["vitest/globals", "vitest/jsdom"]
}
}
2 changes: 2 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "vitest/config"
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin"

export default defineConfig({
test: {
Expand All @@ -9,4 +10,5 @@ export default defineConfig({
reporter: [`text`, `json`, `html`],
},
},
plugins: [vanillaExtractPlugin()],
})
Loading

0 comments on commit 200cfa0

Please sign in to comment.