Skip to content

Commit df7a9ac

Browse files
authored
Add community blog and first blog post (#973)
* add community blog * starting to work on first post * remove Js.Array2 * Bluesky * social link * originalLink * first pass at article * another pass * relative link * fix regex * break up section * pr feedback * no image * relative links * that broke things * pr feedback * typo
1 parent 3eef195 commit df7a9ac

File tree

9 files changed

+252
-29
lines changed

9 files changed

+252
-29
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
author: josh-derocher-vlk
3+
date: "2025-03-03"
4+
title: What can I do with ReScript?
5+
description: |
6+
Can I use Vite, or Next.js? Is it only for React? Can I use Node or Deno?
7+
---
8+
9+
You've taken a look and ReScript and you want to try it out, but how do you get started? There's the [installation](/docs/manual/latest/installation) page in the docs,
10+
which is great if you want to set up a new React app using [create-rescript-app](https://github.com/rescript-lang/create-rescript-app). There's instructions on how to add it to an existing project or set it up manually.
11+
But that doesn't really answer the question "Can I use this with X?".
12+
13+
## You can use ReScript anywhere you can use JavaScript
14+
ReScript is just a language that compiles to JavaScript. Unlike other language like [Elm](https://elm-lang.org/) or [PureScript](https://www.purescript.org/) ReScript doesn't have a recommended framework or independent ecosystem, it's just part of the normal JavaScript world.
15+
16+
Here's a really basic example that you can run in Node after compiling:
17+
18+
```res
19+
// index.res
20+
Console.log("Hello")
21+
```
22+
23+
Just run `node index.res.js` and you'll see "Hello" logged to the console. You can import compiled ReScript into any project that could import JavaScript.
24+
If you can use `.js` or `.mjs` files, you can use ReScript. This does mean that languages with different file formats like Vue or Svelte require you to import the compiled JavaScript instead of writing it directly in the `.vue` or `.svelte` files.
25+
26+
Real world projects are more than JavaScript files that you write; they use libraries and frameworks. This is where [bindings](/docs/manual/latest/external) come into play.
27+
A binding is a way to tell ReScript the types and imports from external JavaScript. You can think of bindings in the same way that you need to create a `*.d.ts` file to add types to a JavaScript library that doesn't use TypeScript.
28+
29+
ReScript has great integration with [React](/docs/react/latest/introduction) and those bindings are kept up to date by the core team, but that doesn't mean you don't have other options!
30+
31+
## Using existing bindings
32+
While ReScript isn't as large as TypeScript it has a small but growing list of bindings you can find on NPM. The website has a [package explorer](/packages) you can use to find official and community maintained bindings.
33+
Many major libraries have existing bindings. Here's a small set of what you can find.
34+
35+
- [Node](https://github.com/TheSpyder/rescript-nodejs)
36+
- [Material UI](https://github.com/cca-io/rescript-mui)
37+
- [Bun](https://github.com/zth/rescript-bun)
38+
- [Deno](https://github.com/tsirysndr/rescript-deno)
39+
- [Deno's Fresh](https://github.com/jderochervlk/rescript-fresh)
40+
- [Vitest](https://github.com/cometkim/rescript-vitest)
41+
- [Rxjs](https://github.com/noble-ai/rescript-rxjs)
42+
- [React Helmet](https://github.com/MoOx/rescript-react-helmet)
43+
- [Jotai](https://github.com/Fattafatta/rescript-jotai)
44+
- [Headless UI](https://github.com/cbowling/rescript-headlessui)
45+
46+
47+
## Using libraries and frameworks created for ReScript
48+
Bindings are great if you want to work with libraries written with JavaScript, but there are great options for libraries and frameworks written with ReScript, which means you don't need bindings.
49+
50+
- [ReScript Schema](https://github.com/DZakh/rescript-schema) - The fastest parser in the entire JavaScript ecosystem with a focus on small bundle size and top-notch DX.
51+
- [rescript-relay](https://github.com/zth/rescript-relay) - This is an amazing way to connect React to Relay and GraphQL
52+
- [rescript-rest](https://github.com/DZakh/rescript-rest) - Fully typed RPC-like client, with no need for code generation!
53+
- [rescript-edgedb](https://github.com/zth/rescript-edgedb) - Use EdgeDB fully type safe in ReScript. Embed EdgeQL right in your ReScript source code.
54+
- [ResX](https://github.com/zth/res-x) - A ReScript framework for building server-driven web sites and applications.
55+
56+
## Creating your own bindings
57+
At some point you will probably have to use a library that doesn't have bindings available. Asking on the [forum](https://forum.rescript-lang.org/) is a great place to start. Someone else might have bindings already in a project that they just haven't published to NPM.
58+
You can also get help and guidance on how to write bindings for what you need. Usually you can figure out what you need from looking at a libraries official docs.
59+
You don't need to write bindings for an entire library, or even for all of a functions arguments. Just write what you need as you go.
60+
61+
Let's take a look at the `format` function from [date-fns](https://date-fns.org/). We can see the [arguments in the docs](https://date-fns.org/v4.1.0/docs/format#arguments), and how it should be imported and used.
62+
```ts
63+
// type signature
64+
function format(
65+
date: string | number | Date,
66+
formatStr: string,
67+
options?: FormatOptions
68+
): string
69+
70+
// how it's imported
71+
import { format } from "date-fns";
72+
73+
// how it's used
74+
const result = format(new Date(2014, 1, 11), 'MM/dd/yyyy')
75+
```
76+
77+
That's all we need to know to write bindings to use this function in ReScript.
78+
The first thing we need to figure out is how to handle the type for what `date-fns` considers to be a `date`, which is `Date | string | number`. In ReScript things can't just be of different types like they can in JavaScript or TypeScript. There are a couple options here; you can make a function for each type such as `formatString` and `formatDate`, or you can create a [variant type](/docs/manual/latest/variant) to map to the possible input types.
79+
Creating a function for each type is simpler, and it's most likely how you will use the library in your project. You probably have a standard type for Dates already. We'll also need a type for `FormatDateOptions` in case we want to pass options. We'll use [labeled argmuments](/docs/manual/latest/function#labeled-arguments) for our binding.
80+
```res
81+
// DateFns.res - you might want to put this in a folder called "bindings" or "external"
82+
type formatDateOptions // we're not even going to add anything to this yet until we need something
83+
84+
@module("date-fns") // this is the import path for the module
85+
external formatString: (
86+
~date: string, // the date string
87+
~formatStr: string, // how we want it formatted
88+
~options: formatDateOptions=?, // =? means the argument is optional
89+
) => string = "format" // "format" is the name of the function we are importing from the module
90+
```
91+
92+
Now we can use the function!
93+
<CodeTab labels={["ReScript", "JS Output"]}>
94+
```res
95+
let formattedDate = DateFns.formatString(~date="2021-09-01", ~formatStr="MMMM dd, yyyy")
96+
```
97+
98+
```js
99+
import * as DateFns from "date-fns";
100+
101+
var formattedDate = DateFns.format("2021-09-01", "MMMM dd, yyyy");
102+
```
103+
</CodeTab>
104+
105+
If we need to use `FormatDateOptions` we can add to our type definition as needed. The first option is `firstWeekContainsDate` which can either be `1` or `4`.
106+
Here's how we could write bindings for that.
107+
```res
108+
@unboxed
109+
type firstWeekContainsDate =
110+
| @as(1) One
111+
| @as(4) Four
112+
113+
type formatDateOptions = {firstWeekContainsDate: firstWeekContainsDate}
114+
```
115+
116+
And when we use it it will output either `1` or `4`.
117+
<CodeTab labels={["ReScript", "JS Output"]}>
118+
```res
119+
let formattedDate = formatString(
120+
~date="2021-09-01",
121+
~formatStr="MMMM dd, yyyy",
122+
~options={firstWeekContainsDate: Four},
123+
)
124+
```
125+
126+
```js
127+
import * as DateFns from "date-fns";
128+
129+
var formattedDate = DateFns.format("2021-09-01", "MMMM dd, yyyy", {
130+
firstWeekContainsDate: 4
131+
});
132+
```
133+
</CodeTab>
134+
135+
You can write new bindings and extend existing types as you need.
136+
137+
## How can I get started?
138+
You can [follow this guide](/docs/manual/v11.0.0/converting-from-js) to add ReScript to an existing JavaScript project to get a feel for how the language works and interacts with JavaScript.
139+
The forum is also a great place to ask questions! Feel free to drop by and ask how to get started with a specific framework or project that you want to work on,
140+
and you'll probably get great advice and information from users who have already used ReScript for something similar.
141+
142+
Happy coding!

pages/blog/community.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import BlogRes from "src/Blog.mjs";
2+
3+
export { getStaticProps_Community as getStaticProps } from "src/Blog.mjs";
4+
5+
export default function Blog(props) {
6+
return <BlogRes {...props} />
7+
}

pages/community/overview.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ News are broadcasted on this site's blog, on Bluesky and X. Some extra, less imp
2323

2424
## Articles
2525

26-
- [Getting rid of your dead code in ReScript](https://dev.to/zth/getting-rid-of-your-dead-code-in-rescript-3mba)
26+
- [Community Blog](/blog/community)
27+
- [Getting rid of your dead code in ReScript](https://dev.to/zth/getting-rid-of-your-dead-code-in-rescript-3mba)
2728
- [Speeding up ReScript compilation using interface files](https://dev.to/zth/speeding-up-rescript-compilation-using-interface-files-4fgn)
2829
- Articles in [awesome-rescript](https://github.com/fhammerschmidt/awesome-rescript#readme)
2930

src/Blog.res

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ module Badge = {
4141
}
4242

4343
type category =
44-
| /** Actually only unarchived */ All
44+
| Official
45+
| Community
4546
| Archived
4647

4748
module CategorySelector = {
4849
@react.component
4950
let make = (~selected: category) => {
50-
let tabs = [All, Archived]
51+
let tabs = [Official, Community, Archived]
5152

5253
<div className="text-16 w-full flex items-center justify-between text-gray-60">
5354
{tabs
@@ -56,7 +57,8 @@ module CategorySelector = {
5657
let isActive = selected == tab
5758
let text = (tab :> string)
5859
let href = switch tab {
59-
| All => "/blog"
60+
| Official => "/blog"
61+
| Community => "/blog/community"
6062
| Archived => "/blog/archived"
6163
}
6264
let className =
@@ -170,7 +172,10 @@ module FeatureCard = {
170172
<div>
171173
<a
172174
className="hover:text-gray-60"
173-
href={"https://x.com/" ++ author.xHandle}
175+
href={switch author.social {
176+
| X(handle) => "https://x.com/" ++ handle
177+
| Bluesky(handle) => "https://bsky.app/profile/" ++ handle
178+
}}
174179
rel="noopener noreferrer">
175180
{React.string(author.fullname)}
176181
</a>
@@ -297,17 +302,26 @@ let default = (props: props): React.element => {
297302
let getStaticProps_All: Next.GetStaticProps.t<props, params> = async _ctx => {
298303
let props = {
299304
posts: BlogApi.getLivePosts(),
300-
category: All,
305+
category: Official,
301306
}
302307

303308
{"props": props}
304309
}
305310

306311
let getStaticProps_Archived: Next.GetStaticProps.t<props, params> = async _ctx => {
307312
let props = {
308-
posts: BlogApi.getArchivedPosts(),
313+
posts: BlogApi.getSpecialPosts("archive"),
309314
category: Archived,
310315
}
311316

312317
{"props": props}
313318
}
319+
320+
let getStaticProps_Community: Next.GetStaticProps.t<props, params> = async _ctx => {
321+
let props = {
322+
posts: BlogApi.getSpecialPosts("community"),
323+
category: Community,
324+
}
325+
326+
{"props": props}
327+
}

src/Blog.resi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ let defaultPreviewImg: string
33
type params
44
type props
55

6-
type category = All | Archived
6+
type category = Official | Community | Archived
77

88
let default: props => React.element
99

1010
let getStaticProps_All: Next.GetStaticProps.t<props, params>
1111
let getStaticProps_Archived: Next.GetStaticProps.t<props, params>
12+
let getStaticProps_Community: Next.GetStaticProps.t<props, params>

src/BlogArticle.res

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ module AuthorBox = {
3939
<div className="w-10 h-10 bg-berry-40 block rounded-full mr-3"> authorImg </div>
4040
<div className="body-sm">
4141
<a
42-
href={"https://x.com/" ++ author.xHandle}
42+
href={switch author.social {
43+
| X(handle) => "https://x.com/" ++ handle
44+
| Bluesky(handle) => "https://bsky.app/profile/" ++ handle
45+
}}
4346
className="hover:text-gray-80"
4447
rel="noopener noreferrer">
4548
{React.string(author.fullname)}
@@ -60,6 +63,7 @@ module BlogHeader = {
6063
~category: option<string>=?,
6164
~description: option<string>,
6265
~articleImg: option<string>,
66+
~originalSrc: option<(string, string)>,
6367
) => {
6468
let date = DateStr.toDate(date)
6569

@@ -88,6 +92,17 @@ module BlogHeader = {
8892
</div>
8993
}
9094
)}
95+
{switch originalSrc {
96+
| None => React.null
97+
| Some("", _) => React.null
98+
| Some(_, "") => React.null
99+
| Some(url, title) =>
100+
<div className="mt-1 mb-8">
101+
<a className="body-sm no-underline text-fire hover:underline" href=url>
102+
{React.string(`Originally posted on ${title}`)}
103+
</a>
104+
</div>
105+
}}
91106
<div className="flex flex-col md:flex-row mb-12">
92107
{Array.map(authors, author =>
93108
<div
@@ -147,7 +162,17 @@ let default = (props: props) => {
147162
: React.null
148163

149164
let content = switch fm {
150-
| Ok({date, author, co_authors, title, description, articleImg, previewImg}) =>
165+
| Ok({
166+
date,
167+
author,
168+
co_authors,
169+
title,
170+
description,
171+
articleImg,
172+
previewImg,
173+
originalSrc,
174+
originalSrcUrl,
175+
}) =>
151176
<div className="w-full">
152177
<Meta
153178
siteName="ReScript Blog"
@@ -163,6 +188,10 @@ let default = (props: props) => {
163188
title
164189
description={description->Null.toOption}
165190
articleImg={articleImg->Null.toOption}
191+
originalSrc={switch (originalSrcUrl->Null.toOption, originalSrc->Null.toOption) {
192+
| (Some(url), Some(title)) => Some(url, title)
193+
| _ => None
194+
}}
166195
/>
167196
</div>
168197
<div className="flex justify-center">

src/common/BlogApi.res

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ type post = {
3939
}
4040

4141
let blogPathToSlug = path => {
42-
path->String.replaceRegExp(%re(`/^(archive\/)?\d\d\d\d-\d\d-\d\d-(.+)\.mdx$/`), "$2")
42+
path
43+
->String.replaceRegExp(%re(`/^(archive\/)?\d\d\d\d-\d\d-\d\d-(.+)\.mdx$/`), "$2")
44+
->String.replaceRegExp(%re(`/^(community\/)?\d\d\d\d-\d\d-\d\d-(.+)\.mdx$/`), "$2")
4345
}
4446

4547
let mdxFiles = dir => {
@@ -49,6 +51,7 @@ let mdxFiles = dir => {
4951
let getAllPosts = () => {
5052
let postsDirectory = Node.Path.join2(Node.Process.cwd(), "_blogposts")
5153
let archivedPostsDirectory = Node.Path.join2(postsDirectory, "archive")
54+
let communityPostsDirectory = Node.Path.join2(postsDirectory, "community")
5255

5356
let nonArchivedPosts = mdxFiles(postsDirectory)->Array.map(path => {
5457
let {GrayMatter.data: data} =
@@ -76,7 +79,20 @@ let getAllPosts = () => {
7679
}
7780
})
7881

79-
Array.concat(nonArchivedPosts, archivedPosts)->Array.toSorted((a, b) =>
82+
let communityPosts = mdxFiles(communityPostsDirectory)->Array.map(path => {
83+
let {GrayMatter.data: data} =
84+
Node.Path.join2(communityPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
85+
switch BlogFrontmatter.decode(data) {
86+
| Error(msg) => Exn.raiseError(msg)
87+
| Ok(d) => {
88+
path: Node.Path.join2("community", path),
89+
frontmatter: d,
90+
archived: false,
91+
}
92+
}
93+
})
94+
95+
Array.concatMany(nonArchivedPosts, [archivedPosts, communityPosts])->Array.toSorted((a, b) =>
8096
String.compare(Node.Path.basename(b.path), Node.Path.basename(a.path))
8197
)
8298
}
@@ -102,24 +118,24 @@ let getLivePosts = () => {
102118
)
103119
}
104120

105-
let getArchivedPosts = () => {
121+
let getSpecialPosts = directory => {
106122
let postsDirectory = Node.Path.join2(Node.Process.cwd(), "_blogposts")
107-
let archivedPostsDirectory = Node.Path.join2(postsDirectory, "archive")
123+
let specialPostsDirectory = Node.Path.join2(postsDirectory, directory)
108124

109-
let archivedPosts = mdxFiles(archivedPostsDirectory)->Array.map(path => {
125+
let specialPosts = mdxFiles(specialPostsDirectory)->Array.map(path => {
110126
let {GrayMatter.data: data} =
111-
Node.Path.join2(archivedPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
127+
Node.Path.join2(specialPostsDirectory, path)->Node.Fs.readFileSync->GrayMatter.matter
112128
switch BlogFrontmatter.decode(data) {
113129
| Error(msg) => Exn.raiseError(msg)
114130
| Ok(d) => {
115-
path: Node.Path.join2("archive", path),
131+
path: Node.Path.join2(directory, path),
116132
frontmatter: d,
117-
archived: true,
133+
archived: directory === "archive",
118134
}
119135
}
120136
})
121137

122-
archivedPosts->Array.toSorted((a, b) =>
138+
specialPosts->Array.toSorted((a, b) =>
123139
String.compare(Node.Path.basename(b.path), Node.Path.basename(a.path))
124140
)
125141
}

src/common/BlogApi.resi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type post = {
66

77
let getAllPosts: unit => array<post>
88
let getLivePosts: unit => array<post>
9-
let getArchivedPosts: unit => array<post>
9+
let getSpecialPosts: string => array<post>
1010
let blogPathToSlug: string => string
1111

1212
module RssFeed: {

0 commit comments

Comments
 (0)