Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/pages/index.tsx
1006 views
1
import { useEffect, useState } from "react"
2
import { FormattedMessage, useIntl } from "react-intl"
3
import Image from "next/legacy/image"
4
import Head from "next/head"
5
import classnames from "classnames"
6
import { useKeenSlider } from "keen-slider/react"
7
import "keen-slider/keen-slider.min.css"
8
import resolveConfig from "tailwindcss/resolveConfig"
9
import twConfig from "../tailwind.config"
10
11
import { withDefaultStaticProps } from "../utils/defaultStaticProps"
12
import LinkButton from "../components/LinkButton"
13
import TestimonialCard from "../components/TestimonialCard"
14
import SponsorLogoGroup from "../components/SponsorLogoGroup"
15
import { IconCard } from "../components/IconCard"
16
17
import testimonials from "../data/testimonials"
18
import { platinum, additionalFunding } from "../data/sponsors"
19
20
import illoTimeline from "../public/illustrations/features_timeline.png"
21
import illoAudience from "../public/illustrations/features_audience.png"
22
import illoModeration from "../public/illustrations/features_moderation.png"
23
import illoCustomization from "../public/illustrations/features_customization.png"
24
import illoWorld from "../public/illustrations/home_sponsors_world.png"
25
26
import homeHeroMobile from "../public/illustrations/home_hero_mobile.webp"
27
import homeHeroDesktop from "../public/illustrations/home_hero_desktop.png"
28
import Hero from "../components/Hero"
29
import { getDirForLocale } from "../utils/locales"
30
import { useRouter } from "next/router"
31
import Layout from "../components/Layout"
32
33
function Home() {
34
const intl = useIntl()
35
36
return (
37
<Layout>
38
<Hero
39
mobileImage={homeHeroMobile}
40
desktopImage={homeHeroDesktop}
41
homepage
42
>
43
<h1 className="h1 mb-4 max-w-[17ch] md:mb-7">
44
<FormattedMessage
45
id="home.hero.headline"
46
defaultMessage="Social networking that's not for sale."
47
/>
48
</h1>
49
50
<p className="sh1 mb-11 max-w-[50ch]">
51
<FormattedMessage
52
id="home.hero.body"
53
defaultMessage="Your home feed should be filled with what matters to you most, not what a corporation thinks you should see. Radically different social media, back in the hands of the people."
54
/>
55
</p>
56
57
<div className="flex flex-wrap justify-center gap-4 md:gap-12">
58
<LinkButton size="large" href="https://mastodon.social/auth/sign_up">
59
<FormattedMessage
60
id="home.join_now"
61
defaultMessage="Join {domain}"
62
values={{ domain: "mastodon.social" }}
63
/>
64
</LinkButton>
65
66
<LinkButton size="large" href="/servers" light borderless>
67
<FormattedMessage
68
id="home.pick_another_server"
69
defaultMessage="Pick another server"
70
/>
71
</LinkButton>
72
</div>
73
</Hero>
74
<Features />
75
<WhyMastodon />
76
<Testimonials testimonials={testimonials} />
77
<Sponsors sponsors={{ platinum, additionalFunding }} />
78
<Head>
79
<title>
80
{`Mastodon - ${intl.formatMessage({
81
id: "home.page_title",
82
defaultMessage: "Decentralized social media",
83
})}`}
84
</title>
85
86
<meta
87
property="og:title"
88
content={`Mastodon - ${intl.formatMessage({
89
id: "home.page_title",
90
defaultMessage: "Decentralized social media",
91
})}`}
92
/>
93
<meta
94
property="og:description"
95
content={intl.formatMessage({
96
id: "home.page_description",
97
defaultMessage:
98
"Learn more about Mastodon, the radically different, free and open-source decentralized social media platform.",
99
})}
100
/>
101
<meta
102
property="description"
103
content={intl.formatMessage({
104
id: "home.page_description",
105
defaultMessage:
106
"Learn more about Mastodon, the radically different, free and open-source decentralized social media platform.",
107
})}
108
/>
109
110
<link rel="me" href="https://mastodon.social/@MastodonEngineering" />
111
<link rel="me" href="https://mastodon.social/@Staff" />
112
<link rel="me" href="https://mastodon.social/@ServerHighlights" />
113
</Head>
114
</Layout>
115
)
116
}
117
118
export default Home
119
120
const Features = () => {
121
return (
122
<section>
123
{[
124
{
125
title: (
126
<FormattedMessage
127
id="home.features.timeline.title"
128
defaultMessage="Stay in control of your own timeline"
129
/>
130
),
131
body: (
132
<FormattedMessage
133
id="home.features.timeline.body"
134
defaultMessage="You know best what you want to see on your home feed. No algorithms or ads to waste your time. Follow anyone across any Mastodon server from a single account and receive their posts in chronological order, and make your corner of the internet a little more like you."
135
/>
136
),
137
button: (
138
<LinkButton
139
size="large"
140
href="https://docs.joinmastodon.org/user/moderating/"
141
>
142
<FormattedMessage
143
id="home.features.button.learn_more"
144
defaultMessage="Learn more"
145
/>
146
</LinkButton>
147
),
148
image: illoTimeline,
149
},
150
{
151
title: (
152
<FormattedMessage
153
id="home.features.audience.title"
154
defaultMessage="Build your audience in confidence"
155
/>
156
),
157
body: (
158
<FormattedMessage
159
id="home.features.audience.body"
160
defaultMessage="Mastodon provides you with a unique possibility of managing your audience without middlemen. Mastodon deployed on your own infrastructure allows you to follow and be followed from any other Mastodon server online and is under no one's control but yours."
161
/>
162
),
163
button: (
164
<LinkButton
165
size="large"
166
href="https://docs.joinmastodon.org/user/run-your-own/"
167
>
168
<FormattedMessage
169
id="home.features.button.learn_more"
170
defaultMessage="Learn more"
171
/>
172
</LinkButton>
173
),
174
image: illoAudience,
175
},
176
{
177
title: (
178
<FormattedMessage
179
id="home.features.moderation.title"
180
defaultMessage="Moderating the way it should be"
181
/>
182
),
183
body: (
184
<FormattedMessage
185
id="home.features.moderation.body"
186
defaultMessage="Mastodon puts decision making back in your hands. Each server creates their own rules and regulations, which are enforced locally and not top-down like corporate social media, making it the most flexible in responding to the needs of different groups of people. Join a server with the rules you agree with, or host your own."
187
/>
188
),
189
button: (
190
<LinkButton size="large" href="/servers">
191
<FormattedMessage
192
id="home.features.button.find_a_server"
193
defaultMessage="Find a server"
194
/>
195
</LinkButton>
196
),
197
image: illoModeration,
198
},
199
{
200
title: (
201
<FormattedMessage
202
id="home.features.self_expression.title"
203
defaultMessage="Unparalleled creativity"
204
/>
205
),
206
body: (
207
<FormattedMessage
208
id="home.features.self_expression.body"
209
defaultMessage="Mastodon supports audio, video and picture posts, accessibility descriptions, polls, content warnings, animated avatars, custom emojis, thumbnail crop control, and more, to help you express yourself online. Whether you're publishing your art, your music, or your podcast, Mastodon is there for you."
210
/>
211
),
212
button: (
213
<LinkButton
214
size="large"
215
href="https://docs.joinmastodon.org/user/posting/"
216
>
217
<FormattedMessage
218
id="home.features.button.learn_more"
219
defaultMessage="Learn more"
220
/>
221
</LinkButton>
222
),
223
image: illoCustomization,
224
},
225
].map((block, i) => {
226
const isOdd = i % 2 != 0
227
return (
228
<div
229
className={classnames("full-width-bg", isOdd && "bg-gray-5")}
230
key={i}
231
>
232
<div className="full-width-bg__inner pt-14 pb-[4.5rem] md:grid md:grid-cols-2 md:items-center md:gap-gutter xl:grid-cols-12">
233
<div
234
className={classnames(
235
"row-span-full xl:col-span-5",
236
isOdd ? "xl:col-start-2" : "order-2 xl:col-start-8"
237
)}
238
>
239
<Image src={block.image} alt="" />
240
</div>
241
242
<div
243
className={classnames(
244
"row-span-full xl:col-span-5",
245
isOdd ? "xl:col-start-8" : "xl:col-start-2"
246
)}
247
>
248
<h2 className="h4 md:h2 mb-2 md:mb-5">{block.title}</h2>
249
<p className="sh1 mb-8 text-gray-1">{block.body}</p>
250
{block.button}
251
</div>
252
</div>
253
</div>
254
)
255
})}
256
</section>
257
)
258
}
259
260
const WhyMastodon = () => {
261
const [currentSlide, setCurrentSlide] = useState(0)
262
const [loaded, setLoaded] = useState(false)
263
const fullConfig = resolveConfig(twConfig)
264
265
const options = {
266
slideChanged(slider) {
267
setCurrentSlide(slider.track.details.rel)
268
},
269
created() {
270
setLoaded(true)
271
},
272
slides: {
273
perView: 1,
274
spacing: 16,
275
},
276
breakpoints: {
277
[`(min-width: ${fullConfig.theme.screens["md"]})`]: {
278
disabled: true,
279
},
280
},
281
}
282
const [sliderRef, instanceRef] = useKeenSlider(options)
283
284
const intl = useIntl()
285
286
return (
287
<section className="py-20">
288
<h3 className="h3 pb-16 text-center">
289
<FormattedMessage id="home.why.title" defaultMessage="Why Mastodon?" />
290
</h3>
291
<div dir="ltr">
292
<div
293
ref={sliderRef}
294
className="keen-slider mb-8 md:mb-0 md:grid md:grid-cols-2 md:gap-gutter lg:grid-cols-4"
295
>
296
<IconCard
297
className="keen-slider__slide shadow-none md:border md:border-gray-3"
298
title={
299
<FormattedMessage
300
id="home.why.decentralized.title"
301
defaultMessage="Decentralized"
302
/>
303
}
304
icon="decentralized"
305
copy={
306
<FormattedMessage
307
id="home.why.decentralized.copy"
308
defaultMessage="Instant global communication is too important to belong to one company. Each Mastodon server is a completely independent entity, able to interoperate with others to form one global social network."
309
/>
310
}
311
/>
312
<IconCard
313
className="keen-slider__slide shadow-none md:border md:border-gray-3"
314
title={
315
<FormattedMessage
316
id="home.why.opensource.title"
317
defaultMessage="Open Source"
318
/>
319
}
320
icon="open-source"
321
copy={
322
<FormattedMessage
323
id="home.why.opensource.copy"
324
defaultMessage="Mastodon is free and open-source software. We believe in your right to use, copy, study and change Mastodon as you see fit, and we benefit from contributions from the community."
325
/>
326
}
327
/>
328
<IconCard
329
className="keen-slider__slide shadow-none md:border md:border-gray-3"
330
title={
331
<FormattedMessage
332
id="home.why.not_for_sale.title"
333
defaultMessage="Not for Sale"
334
/>
335
}
336
icon="price-tag"
337
copy={
338
<FormattedMessage
339
id="home.why.not_for_sale.copy"
340
defaultMessage="We respect your agency. Your feed is curated and created by you. We will never serve ads or push profiles for you to see. That means your data and your time are yours and yours alone."
341
/>
342
}
343
/>
344
<IconCard
345
className="keen-slider__slide shadow-none md:border md:border-gray-3"
346
title={
347
<FormattedMessage
348
id="home.why.interoperability.title"
349
defaultMessage="Interoperable"
350
/>
351
}
352
icon="move"
353
copy={
354
<FormattedMessage
355
id="home.why.interoperability.copy"
356
defaultMessage="Built on open web protocols, Mastodon can speak with any other platform that implements ActivityPub. With one account you get access to a whole universe of social apps—the fediverse."
357
/>
358
}
359
/>
360
</div>
361
{loaded && instanceRef.current && (
362
<div className="flex items-center justify-center gap-2 md:hidden">
363
{instanceRef.current.slides.map((_, idx) => {
364
return (
365
<button
366
key={idx}
367
onClick={() => {
368
instanceRef.current?.moveToIdx(idx)
369
}}
370
className={
371
"rounded-[50%] p-1.5 " +
372
(currentSlide === idx ? "bg-blurple-500" : "bg-gray-3")
373
}
374
aria-label={intl.formatMessage({
375
id: "home.slider.go_to_slide",
376
defaultMessage: "Go to slide",
377
})}
378
></button>
379
)
380
})}
381
</div>
382
)}
383
</div>
384
</section>
385
)
386
}
387
388
const Testimonials = ({ testimonials }) => {
389
const [currentSlide, setCurrentSlide] = useState(0)
390
const [loaded, setLoaded] = useState(false)
391
const fullConfig = resolveConfig(twConfig)
392
393
const options = {
394
loop: true,
395
slideChanged(slider) {
396
setCurrentSlide(slider.track.details.rel)
397
},
398
created() {
399
setLoaded(true)
400
},
401
slides: {
402
perView: 1,
403
spacing: 16,
404
},
405
breakpoints: {
406
[`(min-width: ${fullConfig.theme.screens["md"]})`]: {
407
slides: { perView: 2, spacing: 16 },
408
},
409
[`(min-width: ${fullConfig.theme.screens["lg"]})`]: {
410
slides: { perView: 3, spacing: 16 },
411
},
412
},
413
}
414
const [sliderRef, instanceRef] = useKeenSlider(options)
415
416
const intl = useIntl()
417
418
return (
419
<section className="full-width-bg bg-gray-5 pt-20 pb-28">
420
<div className="full-width-bg__inner">
421
<h2 className="h3 pb-16 text-center">
422
<FormattedMessage
423
id="home.testimonials.title"
424
defaultMessage="What our users are saying"
425
/>
426
</h2>
427
428
<div dir="ltr">
429
<div ref={sliderRef} className="keen-slider mb-8">
430
{testimonials.map((testimonial) => {
431
return (
432
<TestimonialCard
433
key={testimonial.name}
434
testimonial={testimonial}
435
/>
436
)
437
})}
438
</div>
439
{loaded && instanceRef.current && (
440
<div className="flex items-center justify-center gap-2">
441
{testimonials.map((_, idx) => {
442
return (
443
<button
444
key={idx}
445
onClick={() => {
446
instanceRef.current?.moveToIdx(idx)
447
}}
448
className={
449
"rounded-[50%] p-1.5 " +
450
(currentSlide === idx ? "bg-blurple-500" : "bg-gray-3")
451
}
452
aria-label={intl.formatMessage({
453
id: "home.slider.go_to_slide",
454
defaultMessage: "Go to slide",
455
})}
456
></button>
457
)
458
})}
459
</div>
460
)}
461
</div>
462
</div>
463
</section>
464
)
465
}
466
467
const Sponsors = ({ sponsors }) => {
468
return (
469
<section className="grid gap-x-gutter text-center lg:grid-cols-12">
470
<div className="py-20 lg:col-span-12 lg:grid lg:grid-cols-12 lg:gap-x-gutter lg:py-28">
471
<div className="mx-auto mb-12 max-w-lg lg:col-span-4 lg:col-start-5 lg:mb-10 lg:w-full">
472
<Image
473
src={illoWorld}
474
alt="Illustration of elephant characters on a globe."
475
/>
476
</div>
477
478
<div className=" lg:col-span-8 lg:col-start-3">
479
<h2 className="h2 mb-6">
480
<FormattedMessage
481
id="home.sponsors.title"
482
defaultMessage="Independent always"
483
/>
484
</h2>
485
486
<p className="b1 lg:sh1 mb-12 lg:mb-10">
487
<FormattedMessage
488
id="home.sponsors.body"
489
defaultMessage="Mastodon is free and open-source software developed by a non-profit organization. Public support directly sustains development and evolution."
490
/>
491
</p>
492
493
<div className="flex flex-col items-center justify-center gap-6 sm:flex-row">
494
<LinkButton href="https://patreon.com/mastodon" size="large">
495
<FormattedMessage
496
id="sponsors.donate_on_patreon"
497
defaultMessage="Donate on Patreon"
498
/>
499
</LinkButton>
500
501
<LinkButton
502
href="https://donate.stripe.com/00g5l42h8ezY3YcaEE"
503
size="large"
504
>
505
<FormattedMessage
506
id="sponsors.donate_directly"
507
defaultMessage="Donate directly"
508
/>
509
</LinkButton>
510
511
<LinkButton href="/sponsors" light size="large">
512
<FormattedMessage
513
id="sponsors.learn_more"
514
defaultMessage="Learn more"
515
/>
516
</LinkButton>
517
</div>
518
</div>
519
</div>
520
521
<h3 className="h5 mb-8 text-center lg:col-span-12">
522
<FormattedMessage
523
id="sponsors.supported_by"
524
defaultMessage="Supported by"
525
/>
526
</h3>
527
528
<div className="lg:col-start-2 lg:col-end-12">
529
<SponsorLogoGroup sponsors={sponsors.platinum} />
530
</div>
531
532
{sponsors.additionalFunding.length > 0 && (
533
<>
534
<h4 className="h5 mb-8 pt-20 text-center lg:col-span-12">
535
<FormattedMessage
536
id="home.additional_support_from"
537
defaultMessage="Additional support from"
538
/>
539
</h4>
540
541
<div className="lg:col-start-4 lg:col-end-10 lg:mb-16">
542
<SponsorLogoGroup sponsors={sponsors.additionalFunding} />
543
</div>
544
</>
545
)}
546
547
<p className="mt-8 text-gray-2 lg:col-span-12 lg:mb-16">
548
<FormattedMessage
549
id="sponsors.sponsorship.statement"
550
defaultMessage="Sponsorship does not equal influence. Mastodon is fully independent."
551
/>
552
</p>
553
</section>
554
)
555
}
556
557
export const getStaticProps = withDefaultStaticProps()
558
559