Merge pull request #486 from ruisaraiva19/develop
Some checks failed
Release / Semantic Release (push) Has been cancelled

chore: release
This commit is contained in:
Rui Saraiva 2026-07-26 14:59:18 +01:00 committed by GitHub
commit d4c2ef6e2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 229 additions and 85 deletions

View File

@ -290,7 +290,11 @@
margin-right: 2px;
}
&.personGithub {
// Both modifiers do the same job: drop the base class's Twitter blue for the
// neutral ink. Kept as two names rather than one shared class so the markup
// still says which kind of link it is.
&.personGithub,
&.personWebsite {
color: $black;
p {
@ -316,7 +320,8 @@
}
}
:global(.dark) .personTwitter.personGithub p {
:global(.dark) .personTwitter.personGithub p,
:global(.dark) .personTwitter.personWebsite p {
color: $white;
&:hover {
@ -328,6 +333,13 @@
fill: $white;
}
// The GitHub mark is a filled path; the globe is stroked, so it needs the other
// property. Both are set explicitly rather than relying on currentColor, to
// match how the rule above already works.
:global(.dark) .personTwitter.personWebsite p svg {
stroke: $white;
}
.modalFooter {
padding-bottom: 48px;
margin-top: 12px;

View File

@ -7,6 +7,7 @@ import { Typography } from 'components/typography'
import { ToolTitle } from 'components/tool-title'
import { SvgTwitter } from 'components/svgs/svg-twitter'
import { SvgGithub } from 'components/svgs/svg-github'
import { SvgWebsite } from 'components/svgs/svg-website'
import styles from './index.module.scss'
import { SvgFavycon } from 'components/svgs/svg-favycon'
@ -19,26 +20,47 @@ import miguelTeixeira from '../../public/images/people/miguel-teixeira.png'
const Modal = dynamic(() => import('react-modal'))
// A lookup rather than a chain of ternaries in the JSX, because there are three
// kinds of link now. `personTwitter` is the shared base class — it predates the
// other two and carries the layout, so the modifiers only restate the colour.
const socials = {
twitter: {
Icon: SvgTwitter,
url: (handle: string) => `https://twitter.com/${handle}`,
modifier: undefined,
},
github: {
Icon: SvgGithub,
url: (handle: string) => `https://github.com/${handle}`,
modifier: styles.personGithub,
},
website: {
Icon: SvgWebsite,
url: (handle: string) => `https://${handle}`,
modifier: styles.personWebsite,
},
}
const people = [
{
screenName: 'aboutaugusto',
handle: 'aboutaugusto',
name: 'Augusto Lopes',
role: 'Product Designer',
social: 'twitter',
social: 'twitter' as const,
photo: augustoLopes,
},
{
screenName: 'ruisaraiva19',
handle: 'ruisaraiva.dev',
name: 'Rui Saraiva',
role: 'Full-Stack Developer',
social: 'twitter',
role: 'Principal Engineer',
social: 'website' as const,
photo: ruiSaraiva,
},
{
screenName: 'miguellteixeira',
handle: 'miguellteixeira',
name: 'Miguel Teixeira',
role: 'Full-Stack Developer',
social: 'github',
social: 'github' as const,
photo: miguelTeixeira,
},
]
@ -138,56 +160,47 @@ const FavyconInfo = ({
Behind the curtains
</Typography>
<Typography variant="largeBody" weight="medium" className={styles.subTitle}>
For new updates be sure to follow these guys on Twitter.
For new updates be sure to follow these guys.
</Typography>
</div>
<hr className={styles.hr} />
<div className={styles.people}>
{people.map((person) => (
<div key={person.screenName} className={styles.person}>
<div className={styles.personAvatar}>
<Image
src={person.photo}
alt={person.name}
width={56}
height={56}
placeholder="blur"
layout="intrinsic"
/>
</div>
<div>
<Typography variant="largeBody" weight="semiBold">
{person.name}
</Typography>
<Typography variant="regularBody" weight="medium" className={styles.personRole}>
{person.role}
</Typography>
{person.social === 'twitter' ? (
{people.map((person) => {
const { Icon, url, modifier } = socials[person.social]
return (
<div key={person.handle} className={styles.person}>
<div className={styles.personAvatar}>
<Image
src={person.photo}
alt={person.name}
width={56}
height={56}
placeholder="blur"
layout="intrinsic"
/>
</div>
<div>
<Typography variant="largeBody" weight="semiBold">
{person.name}
</Typography>
<Typography variant="regularBody" weight="medium" className={styles.personRole}>
{person.role}
</Typography>
<a
href={`https://twitter.com/${person.screenName}`}
className={styles.personTwitter}
href={url(person.handle)}
className={classnames(styles.personTwitter, modifier)}
target="_blank"
rel="noopener noreferrer"
>
<Typography variant="footer" weight="semiBold">
<SvgTwitter /> {person.screenName}
<Icon /> {person.handle}
</Typography>
</a>
) : (
<a
href={`https://github.com/${person.screenName}`}
className={classnames(styles.personTwitter, styles.personGithub)}
target="_blank"
rel="noopener noreferrer"
>
<Typography variant="footer" weight="semiBold">
<SvgGithub /> {person.screenName}
</Typography>
</a>
)}
</div>
</div>
</div>
))}
)
})}
</div>
<hr className={styles.hr} />
<div className={classnames(styles.footer, styles.modalFooter)}>
@ -202,8 +215,8 @@ const FavyconInfo = ({
GitHub
</a>
. Have feedback? Send it to{' '}
<a className={styles.contact} href="mailto:hello@favycon.app">
hello@favycon.app
<a className={styles.contact} href="mailto:hello@ruisaraiva.dev">
hello@ruisaraiva.dev
</a>
</Typography>
</div>

View File

@ -1,43 +1,80 @@
import Head from 'next/head'
const SITE_URL = 'https://favycon.vercel.app'
const AUTHOR = { name: 'Rui Saraiva', url: 'https://ruisaraiva.dev' }
// The tool credits three people in its About modal, so the structured data says
// the same thing rather than a tidier untruth: Rui as author, the other two as
// contributors, linked where the modal links them.
const CONTRIBUTORS = [
{ name: 'Augusto Lopes', url: 'https://twitter.com/aboutaugusto' },
{ name: 'Miguel Teixeira', url: 'https://github.com/miguellteixeira' },
]
type SEOProps = {
title: string
description: string
}
const SEO = ({ title, description }: SEOProps) => (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png" />
<meta name="apple-mobile-web-app-title" content="Favycon" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileImage" content="/favicon-144x144.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={title} />
<meta property="og:url" content="https://favycon.vercel.app" />
<meta property="og:image" content="https://favycon.vercel.app/share.png?v5" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
)
const SEO = ({ title, description }: SEOProps) => {
const structuredData = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'Favycon',
description,
url: SITE_URL,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
browserRequirements: 'Requires JavaScript',
image: `${SITE_URL}/share.png`,
license: 'https://opensource.org/licenses/MIT',
// It is genuinely free, and Google wants the price stated to believe it.
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
author: { '@type': 'Person', ...AUTHOR },
contributor: CONTRIBUTORS.map((person) => ({ '@type': 'Person', ...person })),
codeRepository: 'https://github.com/ruisaraiva19/favycon',
}
return (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="author" content={AUTHOR.name} />
<link rel="author" href={AUTHOR.url} />
{/* favycon.app has no DNS, so vercel.app is the only hostname this answers on. */}
<link rel="canonical" href={SITE_URL} />
<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180x180.png" />
<meta name="apple-mobile-web-app-title" content="Favycon" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192x192.png" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileImage" content="/favicon-144x144.png" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={title} />
<meta property="og:url" content={SITE_URL} />
<meta property="og:image" content={`${SITE_URL}/share.png?v5`} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
</Head>
)
}
export { SEO }

View File

@ -0,0 +1,22 @@
// Drawn from primitives rather than path data so the geometry is exact: a
// circle, the equator and one meridian. Stroked rather than filled, which is
// why the dark-mode rule for .personWebsite sets `stroke` where .personGithub
// sets `fill`.
const SvgWebsite: React.FC<JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>> = (props) => (
<svg
width={18}
height={18}
viewBox="0 0 18 18"
fill="none"
stroke="currentColor"
strokeWidth={1.4}
strokeLinecap="round"
{...props}
>
<circle cx={9} cy={9} r={7} />
<line x1={2} y1={9} x2={16} y2={9} />
<ellipse cx={9} cy={9} rx={3.2} ry={7} />
</svg>
)
export { SvgWebsite }

View File

@ -18,7 +18,7 @@
],
"author": {
"name": "Rui Saraiva",
"email": "ruisaraiva19@gmail.com",
"email": "hello@ruisaraiva.dev",
"url": "https://ruisaraiva.dev/"
},
"license": "MIT",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

48
public/llms.txt Normal file
View File

@ -0,0 +1,48 @@
# Favycon
> A free online tool that turns one image into a complete set of favicons, in
> every size and format a site needs, as a downloadable zip.
Drag and drop an image and Favycon returns `favicons.zip` plus the markup to
paste into your `<head>`. Nothing is stored: the image is converted in the
request and the zip is streamed straight back.
Site: https://favycon.vercel.app
## Input
- Accepted formats: JPEG, PNG, SVG.
- Minimum dimensions: 310x310. SVG input is exempt, since it is rescaled from
vector.
- With the PWA option enabled the minimum is 512x512, again except for SVG.
## What the zip contains
- `icons/favicon-<size>.png` at 16x16, 32x32, 57x57, 60x60, 70x70, 72x72,
76x76, 96x96, 114x114, 120x120, 144x144, 150x150, 152x152, 180x180, 192x192
and 310x310.
- `icons/favicon.ico`, rendered from a 256x256 master.
- `icons/favicon.svg`, only when the uploaded image was itself an SVG.
- `icons/browserconfig.xml` for IE10+ tiles.
- `icons/manifest.json`, only when the PWA option is on. That option also adds
128x128, 384x384 and 512x512 PNGs.
- `readme.txt` with the HTML to add, written to match the options chosen.
## Facts
- Author: Rui Saraiva (https://ruisaraiva.dev)
- Contributors: Augusto Lopes (product design), Miguel Teixeira
- Built in 2020
- Licence: MIT
- Source: https://github.com/ruisaraiva19/favycon
- Price: free, no account, no upload limit published
- Contact: hello@ruisaraiva.dev
## Notes
- This is a single-page tool. There is no blog, docs site or API documentation
to crawl beyond this file and the repository.
- There is a public HTTP endpoint at `POST /api/favycon`. It takes a multipart
body with an `icon` file field and an optional `pwa` field set to `1`, and
responds with `application/zip`. It is what the page itself calls; it is not
a documented or versioned public API.

View File

@ -1,2 +1,8 @@
User-Agent: *
Allow: /
Allow: /
# Nothing is disallowed. /api/favycon is POST-only and answers a GET with 405,
# which is a clearer signal to a crawler than a rule it has to fetch robots.txt
# to learn. A plain-text description of the tool lives at /llms.txt
Sitemap: https://favycon.vercel.app/sitemap.xml

6
public/sitemap.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://favycon.vercel.app</loc>
</url>
</urlset>