├── .eslintrc.json
├── .gitignore
├── README.md
├── components.json
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── assets
│ └── logo.png
├── src
├── app
│ ├── (component)
│ │ ├── code-block.tsx
│ │ ├── features.block.tsx
│ │ ├── footer.tsx
│ │ ├── gtag-script.tsx
│ │ ├── link-image-preview.tsx
│ │ ├── link-meta-card.tsx
│ │ ├── logo.tsx
│ │ ├── og-form.tsx
│ │ ├── pattern.tsx
│ │ └── preview-list.tsx
│ ├── app
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── opengraph-image.png
│ ├── page.tsx
│ └── twitter-image.png
├── components
│ ├── layout
│ │ └── appbar.layout.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── skeleton.tsx
│ │ └── textarea.tsx
└── lib
│ ├── hooks
│ └── use-meta.ts
│ ├── service
│ └── open-service.ts
│ ├── type
│ └── meta.type.ts
│ ├── utils.ts
│ └── validator.ts
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Meta Toolkit [](https://hits.seeyoufarm.com)
2 | Meta Toolkit — Preview how your webpage will look on social media
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Introduction
10 |
11 | Meta Toolkit makes it simple to preview and edit meta tags for any URL. Pass a website link to instantly see how it will appear in social media previews. Edit metadata to visualize changes in real-time - all in one simple tool.
12 |
13 | ## Local Development
14 |
15 | To run Meta toolkit locally, you will need to clone this repository
16 |
17 | Once that's done, you can use the following commands to run the app locally:
18 |
19 | ```
20 | yarn
21 | yarn build
22 | yarn dev
23 | ```
24 |
25 | ## Tech Stack
26 |
27 | - [Next.js](https://nextjs.org/) – framework
28 | - [Typescript](https://www.typescriptlang.org/) – language
29 | - [Tailwind](https://tailwindcss.com/) – CSS
30 | - [Vercel](https://vercel.com/) – hosting
31 |
32 | ## Contributing
33 |
34 | We love our contributors! Here's how you can contribute:
35 |
36 | - [Open an issue](https://github.com/TheAlphamerc/meta-toolkit/issues) if you believe you've encountered a bug.
37 | - Make a [pull request](https://github.com/TheAlphamerc/meta-toolkit/pull) to add new features/make quality-of-life improvements/fix bugs.
38 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meta-toolkit",
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 | "@radix-ui/react-slot": "^1.0.2",
13 | "@types/node": "20.6.0",
14 | "@types/react": "18.2.21",
15 | "@types/react-dom": "18.2.7",
16 | "autoprefixer": "10.4.15",
17 | "class-variance-authority": "^0.7.0",
18 | "clsx": "^2.0.0",
19 | "eslint": "8.49.0",
20 | "eslint-config-next": "13.4.19",
21 | "lucide-react": "^0.274.0",
22 | "next": "13.4.19",
23 | "postcss": "8.4.29",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "tailwind-merge": "^1.14.0",
27 | "tailwindcss": "3.3.3",
28 | "tailwindcss-animate": "^1.0.7",
29 | "typescript": "5.2.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheAlphamerc/meta-toolkit/29ae2e4fea30087df81b2ae935cc8176b3f91ad6/public/assets/logo.png
--------------------------------------------------------------------------------
/src/app/(component)/code-block.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@/lib/type/meta.type";
2 |
3 | export default function CodeBlock({ meta }: { meta: Meta }) {
4 | return (
5 |
6 |
7 |
8 | Code
9 |
10 |
{
12 | e.preventDefault();
13 | const text = document.getElementById("code")?.innerText;
14 | console.log(text);
15 | navigator.clipboard.writeText(text || "");
16 | // alert("Copied to clipboard");
17 | }}
18 | >
19 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
41 | <!-- HTML Meta Tags -->
42 |
43 | <
44 | title
45 | >
46 | {meta.title}
47 | </
48 | title
49 | >
50 |
51 |
56 |
57 |
58 |
59 |
60 | <!-- Facebook Meta Tags -->
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | <!-- Twitter Meta Tags -->
72 |
73 |
74 |
79 |
80 |
81 |
86 |
91 |
96 |
97 |
98 | <!-- Meta Tags Generated with Meta tool -->
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | function Meta({
108 | tag = "meta",
109 | property,
110 | content,
111 | propertyName = "property",
112 | }: {
113 | tag?: string;
114 | property: string;
115 | propertyName?: string;
116 | content?: string;
117 | }) {
118 | return (
119 |
120 | <
121 | {tag}
122 | {/* PROPERTY */}
123 |
124 | {propertyName}
125 | ="
126 | {property}
127 | "
128 |
129 |
130 | {/* CONTENT */}
131 |
132 | content
133 | ="
134 | {content}
135 | "
136 |
137 | >
138 |
139 |
140 | );
141 | }
142 |
--------------------------------------------------------------------------------
/src/app/(component)/features.block.tsx:
--------------------------------------------------------------------------------
1 | export default function Features() {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | What is an Open Graph
12 |
13 |
14 |
15 | Open Graph allows websites to provide rich media content when
16 | shared on social platforms like Facebook, Twitter, LinkedIn, and
17 | more. By utilizing Open Graph tags in your HTML code, you can
18 | control how information from your website appears when it's
19 | shared across various social media channels.
20 |
21 |
22 |
23 |
24 |
25 |
26 | What is the image size for Open Graph?
27 |
28 |
29 |
30 | Use images that are at least 1080 pixels in width for best
31 | display on high resolution devices. Facebook recommends using
32 | 1:1 images in your ad creatives for better performance with
33 | image link ads.
34 |
35 |
36 |
37 |
38 |
39 |
40 | How to add OG tags in my site?
41 |
42 |
43 |
44 | Use a free tool such as meta-toolkit.vercel.app to generate
45 | opengraph tags for your website, and copy the meta tags into the
46 | <head> section tag.
47 |
48 |
49 |
50 |
51 |
52 |
53 | Why are Open Graph tags important?
54 |
55 |
56 |
57 | People are arguably more likely to see and click shared content
58 | with optimized OG tags, which means more social media traffic to
59 | your website. They make content more eye-catching in social
60 | media feeds
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | function Checkbox() {
69 | return (
70 |
71 |
72 |
79 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/(component)/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Pattern2 } from "./pattern";
2 |
3 | export default function Footer() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 | function OSSSection() {
11 | return (
12 |
13 |
17 |
18 |
19 |
20 | Proudly open-source
21 |
22 |
23 | Our source code is available on GitHub - feel free to read,
24 | review, or contribute to it however you want!
25 |
26 |
27 |
50 |
51 |
52 | © 2023 Toolkit{" "}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/(component)/gtag-script.tsx:
--------------------------------------------------------------------------------
1 | import Script from "next/script";
2 |
3 | function GTagScript() {
4 | const GA_MEASUREMENT_ID = "G-CT2M6878XY";
5 | return (
6 | <>
7 |
10 |
19 | >
20 | );
21 | }
22 |
23 | export default GTagScript;
24 |
--------------------------------------------------------------------------------
/src/app/(component)/link-image-preview.tsx:
--------------------------------------------------------------------------------
1 | import cx from "clsx";
2 | import Image from "next/image";
3 | import { memo } from "react";
4 |
5 | interface ImageViewProps {
6 | src: string;
7 | type?: "small" | "large";
8 | variant?: "twitter" | "facebook" | "linkedin" | "google";
9 | }
10 | export default memo(function LinkImagePreview({
11 | src,
12 | type,
13 | variant,
14 | }: ImageViewProps) {
15 | return (
16 | {
30 | e.currentTarget.src =
31 | "https://placehold.co/360x150/F8F8FF/A9A9A9/png?text=No+Preview";
32 | }}
33 | />
34 | );
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/(component)/link-meta-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useMeta } from "@/lib/hooks/use-meta";
4 | import { Meta } from "@/lib/type/meta.type";
5 | import { cn } from "@/lib/utils";
6 | import Validator from "@/lib/validator";
7 | import cx from "clsx";
8 | import Image from "next/image";
9 | import { useEffect, useState } from "react";
10 |
11 | interface Props {
12 | type?: "small" | "large";
13 | variant?: "twitter" | "facebook" | "linkedin" | "google";
14 | className?: string;
15 | map: Meta;
16 | }
17 |
18 | /**
19 | * Component to display a link preview card. it uses the the useMeta hook to fetch the link preview data
20 | * @param {string} url - The url to preview
21 | */
22 |
23 | export const LinkMetaCard: React.FC = ({
24 | map,
25 | type = "small",
26 | className,
27 | variant = "facebook",
28 | }) => {
29 | // const [map, setMap] = useState({} as LinkMeta);
30 | const [image, setImage] = useState("");
31 | const [title, setTitle] = useState("");
32 | const [siteName, setSiteName] = useState("");
33 | const [url, setUrl] = useState("");
34 | const [description, setDescription] = useState("");
35 |
36 | const { getMetaMap } = useMeta();
37 |
38 | useEffect(() => {
39 | if (!map) return;
40 |
41 | if (map["og:image"]) {
42 | setImage(map["og:image"]);
43 | }
44 | if (map["og:title"]) {
45 | setTitle(map["og:title"]);
46 | } else if (map["title"]) {
47 | setTitle(map["title"]);
48 | } else {
49 | setTitle("No title specified");
50 | }
51 | if (map["og:description"]) {
52 | setDescription(map["og:description"]);
53 | } else {
54 | setDescription("No description specified");
55 | }
56 | if (map["og:site_name"]) {
57 | setSiteName(map["og:site_name"]);
58 | }
59 | if (map["url"]) {
60 | // Extract the hostname from the url
61 | const hostname = map["url"];
62 | setUrl(hostname);
63 | }
64 |
65 | // eslint-disable-next-line react-hooks/exhaustive-deps
66 | }, [map]);
67 |
68 | if (!Validator.hasValue(map)) {
69 | return (
70 |
81 |
88 |
89 | );
90 | }
91 |
92 | if (
93 | !Validator.hasValue(image) &&
94 | !Validator.hasValue(title) &&
95 | !Validator.hasValue(description)
96 | )
97 | return null;
98 |
99 | return (
100 | <>
101 | {
113 | window.open(url, "_blank");
114 | }}
115 | >
116 | {image && variant !== "google" &&
}
117 |
123 |
128 | {title && (
129 |
141 | {title}
142 |
143 | )}
144 |
149 | {description && (
150 |
159 | {description.substring(0, 140)}
160 |
161 | )}
162 |
163 |
164 | >
165 | );
166 | function ImageView({ src }: { src: string }) {
167 | return (
168 | {
182 | e.currentTarget.src =
183 | "https://placehold.co/360x150/F8F8FF/A9A9A9/png?text=No+Preview";
184 | }}
185 | />
186 | );
187 | }
188 | };
189 |
190 | function Hostname({
191 | hostname,
192 | variant,
193 | visible = true,
194 | }: {
195 | hostname: string;
196 | variant: string;
197 | visible: boolean;
198 | }) {
199 | if (!Validator.hasValue(hostname)) {
200 | return null;
201 | }
202 | return (
203 |
216 | {hostname}
217 |
218 | );
219 | }
220 |
--------------------------------------------------------------------------------
/src/app/(component)/logo.tsx:
--------------------------------------------------------------------------------
1 | export default function Logo() {
2 | return (
3 |
4 |
11 |
12 |
31 |
32 |
33 |
38 |
39 |
54 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/(component)/og-form.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import CodeBlock from "./code-block";
3 | import { Meta } from "@/lib/type/meta.type";
4 | import { Textarea } from "@/components/ui/textarea";
5 |
6 | export default function OGForm({
7 | meta,
8 | setMeta,
9 | }: {
10 | meta?: Meta;
11 | setMeta: React.Dispatch>;
12 | }) {
13 | if (!meta) return null;
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | OpenGraph
22 |
23 |
24 | Open Graph tags that are you using in your site
25 |
26 |
27 |
28 | {/* Form */}
29 |
30 |
31 |
32 |
33 | Title
34 |
35 |
36 | {
39 | setMeta({
40 | ...meta,
41 | title: e.target.value,
42 | "og:title": e.target.value,
43 | });
44 | }}
45 | placeholder="Title"
46 | />
47 |
48 |
49 | {meta?.title?.length}
50 |
51 |
52 | Recommended length: 60 characters
53 |
54 |
55 |
56 |
57 |
58 |
59 | Description
60 |
61 |
62 |
84 |
85 |
86 |
87 | Image
88 |
89 |
90 | {
94 | setMeta({
95 | ...meta,
96 | "og:image": e.target.value,
97 | "twitter:image": e.target.value,
98 | });
99 | }}
100 | placeholder="Image Url"
101 | />
102 |
103 | Recommended dimension: 1200 x 630 pixels
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/(component)/pattern.tsx:
--------------------------------------------------------------------------------
1 | export default function Pattern() {
2 | return (
3 |
4 |
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export function Pattern2({ children }: { children?: React.ReactNode }) {
28 | return (
29 |
30 |
31 |
32 |
36 |
37 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
{children}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/(component)/preview-list.tsx:
--------------------------------------------------------------------------------
1 | import { Meta } from "@/lib/type/meta.type";
2 | import { LinkMetaCard } from "./link-meta-card";
3 |
4 | export default function PreviewList({ meta }: { meta?: Meta }) {
5 | return (
6 |
7 |
8 |
Preview
9 |
10 | How your website is displayed on search engines & social media.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
22 | function Card({ name }: { name: string }) {
23 | return (
24 |
25 |
{name}
26 |
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Meta Toolkit",
9 | description: "Preview how your webpage will look on social media",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: {
15 | children: React.ReactNode;
16 | }) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 | import { useMeta } from "@/lib/hooks/use-meta";
6 | import { Meta } from "@/lib/type/meta.type";
7 | import Validator from "@/lib/validator";
8 | import { cx } from "class-variance-authority";
9 | import React, { useEffect, useState } from "react";
10 | import Features from "../(component)/features.block";
11 | import Footer from "../(component)/footer";
12 | import OGForm from "../(component)/og-form";
13 | import Pattern from "../(component)/pattern";
14 | import PreviewList from "../(component)/preview-list";
15 |
16 | export default function App() {
17 | const [meta, setMeta] = useState ();
18 | const [loading, setLoading] = useState(false);
19 | const [url, setUrl] = useState();
20 |
21 | useEffect(() => {
22 | if (window) {
23 | const query = new URLSearchParams(window.location.search);
24 | const url = query.get("url");
25 | if (url) {
26 | setUrl(url);
27 | }
28 | }
29 | }, []);
30 |
31 | React.useEffect(() => {
32 | if (url) {
33 | setLoading(true);
34 | getMetaMap(url)?.then((data: Meta | undefined) => {
35 | console.log(data);
36 | setLoading(false);
37 | if (!Validator.hasValue(data)) {
38 | setMeta({} as Meta);
39 | return;
40 | }
41 | setMeta(data!);
42 | });
43 | } else {
44 | setMeta(undefined);
45 | }
46 | // eslint-disable-next-line react-hooks/exhaustive-deps
47 | }, [url]);
48 |
49 | const { getMetaMap } = useMeta();
50 | return (
51 |
52 |
53 | {/* Input Bar */}
54 |
55 |
56 |
57 |
58 | We handle everything for you
59 |
60 |
61 | See what lies beyond the surface of a link, before you share it
62 | with others through our powerful link previews.
63 |
64 |
65 |
102 |
103 |
104 |
105 |
106 | {loading ? (
107 |
108 |
109 |
110 |
111 |
112 | ) : !meta ? (
113 |
114 | ) : (
115 |
119 | )}
120 |
121 |
122 |
123 |
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheAlphamerc/meta-toolkit/29ae2e4fea30087df81b2ae935cc8176b3f91ad6/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import GTagScript from "./(component)/gtag-script";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 | const title =
8 | "Meta Toolkit — Preview how your webpage will look on social media";
9 | const description =
10 | "With Meta Toolkit you can preview how your webpage will look on Twitter, Facebook, Twitter, Linkedin and more!";
11 | const url = new URL("https://meta-toolkit.vercel.app");
12 |
13 | export const metadata: Metadata = {
14 | title: title,
15 | description: description,
16 | openGraph: {
17 | title: title,
18 | description: description,
19 | url: url.href,
20 | type: "website",
21 | siteName: "Meta Toolkit",
22 | },
23 | twitter: {
24 | title: title,
25 | description: description,
26 | card: "summary_large_image",
27 | },
28 | applicationName: "Meta Toolkit",
29 | authors: [
30 | {
31 | name: "Sonu Sharma",
32 | url: "https://twitter.com/thealphamerc",
33 | },
34 | ],
35 | colorScheme: "light",
36 | abstract: "Preview how your webpage will look on social media",
37 | keywords: ["meta", "preview", "social media", "twitter", "facebook"],
38 | themeColor: "#ffffff",
39 | };
40 |
41 | export default function RootLayout({
42 | children,
43 | }: {
44 | children: React.ReactNode;
45 | }) {
46 | return (
47 |
48 |
49 | {children}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheAlphamerc/meta-toolkit/29ae2e4fea30087df81b2ae935cc8176b3f91ad6/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { NavbarLayout } from "@/components/layout/appbar.layout";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 | import App from "./app/page";
5 |
6 | export default function Home() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Meta Toolkit
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheAlphamerc/meta-toolkit/29ae2e4fea30087df81b2ae935cc8176b3f91ad6/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/components/layout/appbar.layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface NavbarLayoutProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | /**
8 | * NavbarLayout is a layout component that wraps navbar and content
9 | * @param children
10 | */
11 |
12 | export const NavbarLayout = ({ children }: NavbarLayoutProps) => {
13 | return (
14 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/lib/hooks/use-meta.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { OpenService } from "../service/open-service";
3 | import { Meta } from "../type/meta.type";
4 |
5 | export const useMeta = (): {
6 | getMetaMap: (url?: string) => Promise | null;
7 | } => {
8 | const [metaMap, setMetaMap] = useState>({});
9 |
10 | const fetchData = async (url: string): Promise => {
11 | try {
12 | const response = await OpenService.getLinkMetaData(url);
13 |
14 | return response["result"];
15 | } catch (error) {
16 | console.error(error);
17 | return undefined;
18 | }
19 | };
20 |
21 | async function getMetaMap(url?: string): Promise {
22 | if (!url) return Promise.resolve(null);
23 |
24 | // Check for the blacklist urls
25 | const isBlacklisted = blacklistUrls.some((blacklistUrl) =>
26 | url.includes(blacklistUrl)
27 | );
28 |
29 | if (isBlacklisted) {
30 | return Promise.resolve(null);
31 | }
32 |
33 | // Check for valid link
34 | const isValidLink = url.match(/^(ftp|http|https|www):\/\/[^ "]+$/);
35 | if (!isValidLink) {
36 | return Promise.resolve(null);
37 | }
38 |
39 | // Check for the map if the data is already present
40 | if (metaMap[url]) {
41 | return Promise.resolve(metaMap[url]);
42 | }
43 |
44 | // Check for the storage if the data is already present
45 | const storageMeta = localStorage.getItem("meta");
46 | if (storageMeta) {
47 | const storageMetaMap = JSON.parse(storageMeta) as Record<
48 | string,
49 | Meta
50 | >;
51 | if (storageMetaMap && storageMetaMap[url]) {
52 | return Promise.resolve(storageMetaMap[url]);
53 | }
54 | }
55 |
56 | const meta = await fetchData(url);
57 |
58 | if (meta) {
59 | updateLocalStorage(url, meta);
60 | return Promise.resolve(meta);
61 | }
62 | updateLocalStorage(url, null);
63 |
64 | return Promise.resolve(null);
65 | }
66 |
67 | function updateLocalStorage(url: string, meta: Meta | null) {
68 | // Append meta record to the existing storage
69 | const storageMeta = localStorage.getItem("meta");
70 | if (storageMeta) {
71 | const metaMap = JSON.parse(storageMeta) as Record;
72 | // console.log("🚀 ~ metaMap Pre save map:", metaMap)
73 | const obj = Object.assign(metaMap, { [url]: meta });
74 | localStorage.setItem("meta", JSON.stringify(obj));
75 | // console.log('meta Post save map', { obj, string: JSON.stringify(obj) });
76 | } else {
77 | const obj = Object.assign({}, { [url]: meta });
78 | localStorage.setItem("meta", JSON.stringify(obj));
79 | // console.log('meta', { obj, string: JSON.stringify(obj) });
80 | }
81 | setMetaMap(metaMap);
82 | }
83 |
84 | return { getMetaMap };
85 | };
86 |
87 | const blacklistUrls = [
88 | "www.google.com",
89 | "api.whatsapp.com",
90 | "api.pensil.in",
91 | "livemeeting.pro",
92 | "pensil-social.s3",
93 | "googleusercontent.com",
94 | ".png",
95 | ".jpg",
96 | ".jpeg",
97 | ".gif",
98 | ".bmp",
99 | ".svg",
100 | ".ico",
101 | ".tif",
102 | ".tiff",
103 | ".psd",
104 | ".ai",
105 | ".eps",
106 | ".doc",
107 | ".docx",
108 | ".pdf",
109 | ".ppt",
110 | ".pptx",
111 | ".xls",
112 | ".xlsx",
113 | ".csv",
114 | ".txt",
115 | ".zip",
116 | ".rar",
117 | ".tar",
118 | ".gz",
119 | ".7z",
120 | ".mp3",
121 | ".mp4",
122 | ".avi",
123 | ".wmv",
124 | ".mov",
125 | ".mkv",
126 | ".flv",
127 | ".wav",
128 | ".ogg",
129 | ".ogv",
130 | ".oga",
131 | ".webm",
132 | ".m4a",
133 | ".m4v",
134 | ".mpg",
135 | ".mpeg",
136 | ".m2v",
137 | ".mpg",
138 | ".mpeg",
139 | ".m2v",
140 | ];
141 |
142 |
143 |
--------------------------------------------------------------------------------
/src/lib/service/open-service.ts:
--------------------------------------------------------------------------------
1 |
2 | class OpenService {
3 |
4 | static async getLinkMetaData(url: string) {
5 | const res = await fetch(`https://web-scrapper-coral.vercel.app/api/read_web_meta_data?url=${url}`, {
6 | headers: {
7 | "Access-Control-Allow-Origin": "*",
8 | }
9 | })
10 | if (!res.ok) {
11 | // This will activate the closest `error.js` Error Boundary
12 | throw new Error('Failed to fetch data')
13 | }
14 | return res.json()
15 | }
16 | }
17 |
18 | export { OpenService };
19 |
--------------------------------------------------------------------------------
/src/lib/type/meta.type.ts:
--------------------------------------------------------------------------------
1 | export interface Meta {
2 | "application-name": string;
3 | date: string;
4 | description: string;
5 | keywords: string;
6 | "og:description": string;
7 | "og:image": string;
8 | "og:site_name": string;
9 | "og:title": string;
10 | "og:type": string;
11 | "og:url": string;
12 | title: string;
13 | "twitter:card": string;
14 | "twitter:creator": string;
15 | "twitter:description": string;
16 | "twitter:site": string;
17 | "twitter:title": string;
18 | "twitter:url": string;
19 | "twitter:image": string;
20 | "twitter:domain": string;
21 | url: string;
22 | }
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/validator.ts:
--------------------------------------------------------------------------------
1 | export default class Validator {
2 | /**
3 | * Checks if the given object is not null, not undefined and not empty.
4 | * @param obj The object to check.
5 | * @returns True if the object is not null or undefined and not empty, false otherwise.
6 | * @memberof Validator
7 | * @example
8 | * Validator.hasValue(null); // false
9 | * Validator.hasValue(undefined); // false
10 | * Validator.hasValue({}); // false
11 | * Validator.hasValue({ a: 1 }); // true
12 | * Validator.hasValue([]); // false
13 | * Validator.hasValue([1]); // true
14 | * Validator.hasValue(''); // false
15 | * Validator.hasValue('a'); // true
16 | * Validator.hasValue(0); // true
17 | * Validator.hasValue(false); // true
18 | * Validator.hasValue(true); // true
19 | * Validator.hasValue(NaN); // true
20 | * Validator.hasValue(Infinity); // true
21 | * Validator.hasValue(-Infinity); // true
22 | */
23 | static hasValue(obj: any): boolean {
24 | if (obj === null || obj === undefined) {
25 | return false;
26 | }
27 | if (typeof obj === "object") {
28 | if (Array.isArray(obj)) {
29 | return obj.length > 0;
30 | } else {
31 | return Object.keys(obj).length > 0;
32 | }
33 | }
34 | if (Array.isArray(obj)) {
35 | return obj.length > 0;
36 | }
37 | if (typeof obj === "string") {
38 | return obj.length > 0;
39 | }
40 | return true;
41 | }
42 |
43 | /**
44 | * Check if given url is a valid url.
45 | * @param url The url to check.
46 | * @returns True if the url is valid, false otherwise.
47 | * @memberof Validator
48 | * @example
49 | * Validator.isUrl('https://www.google.com'); // true
50 | * Validator.isUrl('http://www.google.com'); // true
51 | * Validator.isUrl('www.google.com'); // true
52 | * Validator.isUrl('google.com'); // false
53 | * Validator.isUrl('google'); // false
54 | * Validator.isUrl(''); // false
55 | * Validator.isUrl(null); // false
56 | * Validator.isUrl(undefined); // false
57 | * Validator.isUrl(1); // false
58 | * Validator.isUrl({}); // false
59 | * Validator.isUrl([]); // false
60 | * Validator.isUrl(true); // false
61 | */
62 | static isUrl(url: string): boolean {
63 | if (!this.hasValue(url)) {
64 | return false;
65 | }
66 | const pattern = new RegExp(
67 | "^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))" +
68 | "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" +
69 | "(\\?[;&a-z\\d%_.~+=-]*)?" +
70 | "(\\#[-a-z\\d_]*)?$",
71 | "i"
72 | );
73 | return pattern.test(url);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------