├── .eslintrc.json
├── public
├── favicon.ico
├── vercel.svg
├── thirteen.svg
└── next.svg
├── screenshots
└── shiki-safari.png
├── pages
├── _app.tsx
├── _document.tsx
├── api
│ └── hello.ts
└── index.tsx
├── next.config.js
├── .gitignore
├── lib
└── shiki.ts
├── package.json
├── tsconfig.json
├── README.md
└── styles
├── globals.css
└── Home.module.css
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shikijs/next-shiki/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/screenshots/shiki-safari.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shikijs/next-shiki/HEAD/screenshots/shiki-safari.png
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import type { AppProps } from 'next/app'
3 |
4 | export default function App({ Component, pageProps }: AppProps) {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | experimental: {
5 | serverComponentsExternalPackages: ['shiki', 'vscode-oniguruma']
6 | }
7 | }
8 |
9 | module.exports = nextConfig
10 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next'
3 |
4 | type Data = {
5 | name: string
6 | }
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' })
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/lib/shiki.ts:
--------------------------------------------------------------------------------
1 | import type { Highlighter, Lang, Theme } from 'shiki'
2 | import { renderToHtml, getHighlighter } from 'shiki'
3 |
4 | let highlighter: Highlighter
5 | export async function highlight(code: string, theme: Theme, lang: Lang) {
6 | if (!highlighter) {
7 | highlighter = await getHighlighter({
8 | langs: [lang],
9 | theme: theme
10 | })
11 | }
12 |
13 | const tokens = highlighter.codeToThemedTokens(code, lang, theme, {
14 | includeExplanation: false
15 | })
16 | const html = renderToHtml(tokens, { bg: 'transparent' })
17 |
18 | return html
19 | }
20 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-shiki",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/font": "13.1.5",
13 | "@types/node": "18.11.18",
14 | "@types/react": "18.0.27",
15 | "@types/react-dom": "18.0.10",
16 | "eslint": "8.32.0",
17 | "eslint-config-next": "13.1.5",
18 | "next": "13.1.5",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "shiki": "^0.12.1",
22 | "typescript": "4.9.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | Modified to work with [Shiki](https://github.com/shikijs/shiki).
4 |
5 | ## Getting Started
6 |
7 | First, run the development server:
8 |
9 | ```bash
10 | npm run dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000). You should see
14 |
15 | 
16 |
17 | ## Diffs
18 |
19 | Refer to this commit for full diff: [b2639c7](https://github.com/shikijs/next-shiki/commit/b2639c704c0e2eab91c55c08a0419e6b502229eb).
20 |
21 | Notably, you need to set `serverComponentsExternalPackages` in `next.config.js`
22 |
23 | ```diff
24 | /** @type {import('next').NextConfig} */
25 | const nextConfig = {
26 | reactStrictMode: true,
27 | + experimental: {
28 | + serverComponentsExternalPackages: ['shiki', 'vscode-oniguruma']
29 | + }
30 | }
31 |
32 | module.exports = nextConfig
33 | ```
34 |
35 | `lib/shiki.ts`
36 |
37 | ```ts
38 | import type { Highlighter, Lang, Theme } from 'shiki'
39 | import { renderToHtml, getHighlighter } from 'shiki'
40 |
41 | let highlighter: Highlighter
42 | export async function highlight(code: string, theme: Theme, lang: Lang) {
43 | if (!highlighter) {
44 | highlighter = await getHighlighter({
45 | langs: [lang],
46 | theme: theme
47 | })
48 | }
49 |
50 | const tokens = highlighter.codeToThemedTokens(code, lang, theme, {
51 | includeExplanation: false
52 | })
53 | const html = renderToHtml(tokens, { bg: 'transparent' })
54 |
55 | return html
56 | }
57 | ```
58 |
59 | `pages/index.tsx`
60 |
61 | ```tsx
62 | interface Props {
63 | highlightedHtml: string
64 | }
65 |
66 | export default function Home(props: Props) {
67 | return (
68 | <>
69 |
73 | <>
74 | )
75 | }
76 |
77 | export const getServerSideProps: GetServerSideProps = async () => {
78 | const html = await highlight(
79 | `console.log('hi next.js')`,
80 | 'github-dark',
81 | 'javascript'
82 | )
83 |
84 | return {
85 | props: {
86 | highlightedHtml: html
87 | }
88 | }
89 | }
90 | ```
91 |
92 | ## License
93 |
94 | MIT © [Pine Wu](https://github.com/octref)
95 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | }
42 |
43 | @media (prefers-color-scheme: dark) {
44 | :root {
45 | --foreground-rgb: 255, 255, 255;
46 | --background-start-rgb: 0, 0, 0;
47 | --background-end-rgb: 0, 0, 0;
48 |
49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50 | --secondary-glow: linear-gradient(
51 | to bottom right,
52 | rgba(1, 65, 255, 0),
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0.3)
55 | );
56 |
57 | --tile-start-rgb: 2, 13, 46;
58 | --tile-end-rgb: 2, 5, 19;
59 | --tile-border: conic-gradient(
60 | #ffffff80,
61 | #ffffff40,
62 | #ffffff30,
63 | #ffffff20,
64 | #ffffff10,
65 | #ffffff10,
66 | #ffffff80
67 | );
68 |
69 | --callout-rgb: 20, 20, 20;
70 | --callout-border-rgb: 108, 108, 108;
71 | --card-rgb: 100, 100, 100;
72 | --card-border-rgb: 200, 200, 200;
73 | }
74 | }
75 |
76 | * {
77 | box-sizing: border-box;
78 | padding: 0;
79 | margin: 0;
80 | }
81 |
82 | html,
83 | body {
84 | max-width: 100vw;
85 | overflow-x: hidden;
86 | }
87 |
88 | body {
89 | color: rgb(var(--foreground-rgb));
90 | background: linear-gradient(
91 | to bottom,
92 | transparent,
93 | rgb(var(--background-end-rgb))
94 | )
95 | rgb(var(--background-start-rgb));
96 | }
97 |
98 | a {
99 | color: inherit;
100 | text-decoration: none;
101 | }
102 |
103 | @media (prefers-color-scheme: dark) {
104 | html {
105 | color-scheme: dark;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import Image from 'next/image'
3 | import { Inter } from '@next/font/google'
4 | import styles from '@/styles/Home.module.css'
5 | import { highlight } from '@/lib/shiki'
6 | import { GetServerSideProps } from 'next'
7 |
8 | const inter = Inter({ subsets: ['latin'] })
9 |
10 | interface Props {
11 | highlightedHtml: string
12 | }
13 |
14 | export default function Home(props: Props) {
15 | return (
16 | <>
17 |
18 | Create Next App
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Get started by editing
27 | pages/index.tsx
28 |
29 |
46 |
47 |
48 |
55 |
56 |
114 |
115 | >
116 | )
117 | }
118 |
119 | export const getServerSideProps: GetServerSideProps = async () => {
120 | const html = await highlight(
121 | `console.log('hi next.js')`,
122 | 'github-dark',
123 | 'javascript'
124 | )
125 |
126 | return {
127 | props: {
128 | highlightedHtml: html
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | width: var(--max-width);
46 | max-width: 100%;
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo,
108 | .thirteen {
109 | position: relative;
110 | }
111 |
112 | .thirteen {
113 | display: flex;
114 | justify-content: center;
115 | align-items: center;
116 | width: 75px;
117 | height: 75px;
118 | padding: 25px 10px;
119 | margin-left: 16px;
120 | transform: translateZ(0);
121 | border-radius: var(--border-radius);
122 | overflow: hidden;
123 | box-shadow: 0px 2px 8px -1px #0000001a;
124 | }
125 |
126 | .thirteen::before,
127 | .thirteen::after {
128 | content: '';
129 | position: absolute;
130 | z-index: -1;
131 | }
132 |
133 | /* Conic Gradient Animation */
134 | .thirteen::before {
135 | animation: 6s rotate linear infinite;
136 | width: 200%;
137 | height: 200%;
138 | background: var(--tile-border);
139 | }
140 |
141 | /* Inner Square */
142 | .thirteen::after {
143 | inset: 0;
144 | padding: 1px;
145 | border-radius: var(--border-radius);
146 | background: linear-gradient(
147 | to bottom right,
148 | rgba(var(--tile-start-rgb), 1),
149 | rgba(var(--tile-end-rgb), 1)
150 | );
151 | background-clip: content-box;
152 | }
153 |
154 | /* Enable hover only on non-touch devices */
155 | @media (hover: hover) and (pointer: fine) {
156 | .card:hover {
157 | background: rgba(var(--card-rgb), 0.1);
158 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
159 | }
160 |
161 | .card:hover span {
162 | transform: translateX(4px);
163 | }
164 | }
165 |
166 | @media (prefers-reduced-motion) {
167 | .thirteen::before {
168 | animation: none;
169 | }
170 |
171 | .card:hover span {
172 | transform: none;
173 | }
174 | }
175 |
176 | /* Mobile */
177 | @media (max-width: 700px) {
178 | .content {
179 | padding: 4rem;
180 | }
181 |
182 | .grid {
183 | grid-template-columns: 1fr;
184 | margin-bottom: 120px;
185 | max-width: 320px;
186 | text-align: center;
187 | }
188 |
189 | .card {
190 | padding: 1rem 2.5rem;
191 | }
192 |
193 | .card h2 {
194 | margin-bottom: 0.5rem;
195 | }
196 |
197 | .center {
198 | padding: 8rem 0 6rem;
199 | }
200 |
201 | .center::before {
202 | transform: none;
203 | height: 300px;
204 | }
205 |
206 | .description {
207 | font-size: 0.8rem;
208 | }
209 |
210 | .description a {
211 | padding: 1rem;
212 | }
213 |
214 | .description p,
215 | .description div {
216 | display: flex;
217 | justify-content: center;
218 | position: fixed;
219 | width: 100%;
220 | }
221 |
222 | .description p {
223 | align-items: center;
224 | inset: 0 0 auto;
225 | padding: 2rem 1rem 1.4rem;
226 | border-radius: 0;
227 | border: none;
228 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
229 | background: linear-gradient(
230 | to bottom,
231 | rgba(var(--background-start-rgb), 1),
232 | rgba(var(--callout-rgb), 0.5)
233 | );
234 | background-clip: padding-box;
235 | backdrop-filter: blur(24px);
236 | }
237 |
238 | .description div {
239 | align-items: flex-end;
240 | pointer-events: none;
241 | inset: auto 0 0;
242 | padding: 2rem;
243 | height: 200px;
244 | background: linear-gradient(
245 | to bottom,
246 | transparent 0%,
247 | rgb(var(--background-end-rgb)) 40%
248 | );
249 | z-index: 1;
250 | }
251 | }
252 |
253 | /* Tablet and Smaller Desktop */
254 | @media (min-width: 701px) and (max-width: 1120px) {
255 | .grid {
256 | grid-template-columns: repeat(2, 50%);
257 | }
258 | }
259 |
260 | @media (prefers-color-scheme: dark) {
261 | .vercelLogo {
262 | filter: invert(1);
263 | }
264 |
265 | .logo,
266 | .thirteen img {
267 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
268 | }
269 | }
270 |
271 | @keyframes rotate {
272 | from {
273 | transform: rotate(360deg);
274 | }
275 | to {
276 | transform: rotate(0deg);
277 | }
278 | }
279 |
--------------------------------------------------------------------------------