Anker is a very light weight template for social media bio links like Linktree. The template is very little SASS and JS code that anybody can customize in few minutes. With Anker you can make Landing Pages super fast and super easy.
- Demo
- Why not just use Linktree or similar apps?
- The Codebase
- How do I make my own themes
- I want more customization
- Deployment
- Contributing to Anker
See the Demo Here with the default theme!
You can use Linktree or other similar apps if you want but there are some reasons why you don't have to or don't want to.
- Linktree and similar apps don't allow you to customize your theme
- If you want better themes you can't get them at least not for free
- You can't use your own DOMAIN NAME
- As developers we like customizability and like to make our own things. If you want to make your own theme well, you can't.
- If your client wants a bio link you can just use Anker and customize it to
suit their needs. Also you can use their domain name and not some weird
websites that have extensions like
.ee
,.bio
,.site
etc. It can be done in few minutes and can easily make you some extra $$. - You can use this template and make your favorite theme and sell it on Gumtree or similar apps. This however is not the motivation behind Anker but it certainly is a possibility.
- As developers we have our own domain names and can design better landing
pages. it's always better to have
john.com/bio
thanlinktr.ee/john
orbio.site/john
.
Finally, you can now make your own bio links/ landing pages for free in a matter of minutes.
The whole template is made up of some SASS and JS files. The data is served from a JSON file. The JSON file can either be stored locally or served from somewhere else ideally raw JSON from GitHub. The only problem with reading raw data from GitHub is that it might be slower than reading from local file.
The best option however is services like Netlify, Cloudflare pages etc. Netlify's
starter plan allows you to host your data/website for free. You can host both your
data.json
file and profile picture. The best part is that your data is hosted on
their CDN so it's super fast. Netlify
Solution
First, clone this repo. Run the following command to install all the dependencies.
npm install
After the dependencies are installed you want to watch for changes and start a live server by running the command below.
npm run start
Now you want to navigate to sass/
directory and start customizing.
.
├── abstracts
│  ├── _fonts.scss
│  ├── _index.scss
│  ├── _mixins.scss
│  └── _variables.scss
├── base
│  ├── _base.scss
│  ├── _grid.scss
│  ├── _index.scss
│  └── _typography.scss
├── layout
│  ├── _body.scss
│  ├── _footer.scss
│  ├── _header.scss
│  └── _index.scss
├── main.scss
└── themes
├── _classic-dark.scss
├── _classic-light.scss
├── _index.scss
├── _modern-dark.scss
└── _modern-light.scss
The file tree above shows a directory called themes/
with some themes inside.
Those are your theme files you can create as many themes you want. There isn't
much to be changed. The whole template has a background, profile picture, name
and the links. Inside your theme file you can change all those things. See the
code below:
@use '../abstracts' as *;
// Here you can define your own variables for colors etc
body {
color: var(--color-primary);
background-color: var(--color-background);
font-weight: var(--font-weight-regular);
}
.logo {
fill: var(--color-primary);
}
.links {
list-style: none;
}
.link {
&:link,
&:visited {
text-decoration: none;
display: block;
color: var(--color-white);
background-color: var(--color-primary);
padding: 1.5rem;
border-radius: .5rem;
text-align: center;
}
}
.link-item {
&:not(:last-child) {
margin-bottom: 1.5rem;
}
}
.social-links {
list-style: none;
display: flex;
margin-top: 5rem;
grid-column-gap: 2rem;
justify-content: center;
}
.social-link {
&:link,
&:visited {
text-decoration: none;
display: block;
color: var(--color-primary);
}
}
.social-icon {
width: 25px;
height: 25px;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
cursor: pointer;
}
Before you start customizing add your theme file to _index.scss
inside
themes
directory so it can be compiled for use. You only have to add the theme
you want to use not all of them if you have more than one. Also copy data.json
file to your dist so you have data to test during development. Normally inside
src/js/config.js
the data will be linked.
See the content of src/themes/_index.scss
file below:
@forward "classic-dark";
-
For the
background
you can have whatever you want simple one color, gradient, gradient mesh, image, video etc. -
The
.logo
class sets the fill property of the Anker logo in footer. The logo has to be in the footer. -
The
.link
class is the one you want to change. You can add hover effects to it, animation or even make it drop-down to show some content. -
For the
social-link
you also don't have to do much except for the color. if the color is the same as the primary color you can just inherit it from parent element. -
This is not all. You can also customize the JS theme file. You might want to add description above the links, embed a video or audio, add a mailing list subscription form etc. See the JS file customization here!
And that's it you now have your own theme. But wait you have to link your
JSON file. If you want to serve the JSON file locally then add your links to
data.json
file and npm run build
will copy it to dist/
directory.
Otherwise you can serve it from somewhere else.
Note:
npm run build
will not copy thedata.json
file on windows. You might wanna copy it manually.
If you want to publish your theme read these guidelines.
The JSON file is very straight forward. name
is your name you want to show.
links
are the links that are listed on your bio and socials
are obviously
your social links object.
{
"name": "John Doe",
"profilePic": "https://ankerdata.netlify.app/profile-img-logo-only.png",
"links": [
{
"title": "Link One",
"link": "https://link.com/"
},
{
"title": "Link Two",
"link": "https://link.com/"
},
{
"title": "Link Three",
"link": "https://link.com/"
},
{
"title": "Link Four",
"link": "https://link.com/"
},
{
"title": "Link Five",
"link": "https://link.com/"
},
{
"title": "Link Six",
"link": "https://link.com/"
},
{
"title": "Link Seven",
"link": "https://link.com/"
}
],
"socials": [
{
"title": "github",
"link": "https://github.com/0xkhan"
},
{
"title": "dribbble",
"link": "https://dribbble.com/0xkhani"
},
{
"title": "twitter",
"link": "https://twitter.com/0xkhani"
},
{
"title": "instagram",
"link": "https://instagram.com/0xkhani"
}
]
}
The above JSON file is for the basic theme. For the Pro version of the themes which include dropdown apps you'll have to add some extra information for the apps to be recognized. See the key changes below:
{
{ ... },
"links": [
{
"title": "Link One",
"link": "https://link.com/"
},
{
"title": "Link Two",
"link": "https://www.youtube.com/embed/gdLLRj1Ge7g",
"type": "dropdown",
"app": "youtube"
},
{
"title": "Link Three",
"link": "https://link.com/"
},
{
"title": "Link Four",
"link": "https://link.com/",
"type": "dropdown",
"app": "spotify"
}
],
{ ... }
}
After you're finished with everything just build the project by running npm run build
. Now you can take your dist/
directory and host it on your server. See
the Netlify solution below.
Instead of JSON file you can also use Google Sheets as backend for your Anker landing page. Here is an example of how your Google sheet will look like.
You have to set your Google Sheet permissions read-only
for everybody.
Note: ONLY read permissions should be global not edit otherwise anybody can edit your Google sheet!
Inside the src/js/config.js
file set your Google Sheet ID so it can be read by
Anker and fetch your data from your sheet.
export const JSON_URL = null;
export const SHEET_ID = "1tdRaVMG9BSry5SpJCH09VkslR1Sc7tPYH4b5m-RJn9c";
export const TIMEOUT_SEC = 10;
export const THEME = "default-theme-pro";
How to get your sheet ID? SHEET_ID
is the ID you get from your sheet URL for
example:
https://docs.google.com/spreadsheets/d/1tdRaVMG9BSry5SpJCH09VkslR1Sc7tPYH4b5m-RJn9c/edit#gid=0
Your sheet SHEET_ID
is inbetween /d/<sheetId>/edit#
. In the above URL the
SHEET_ID
is:
1tdRaVMG9BSry5SpJCH09VkslR1Sc7tPYH4b5m-RJn9c
- Create a directory e.g
my-anker-data/
. See My Data Repo - Copy your
data.json
file tomy-anker-data/
- Initiate a git repository
- Push the repo to GitHub, GitLab, Bitbucket
- Link a new project on Netlify to this repo and deploy
After the data is deployed when you go to
<netlify-subdomain>.netlify.app/data.json
you should be seeing your JSON data.
However you're not done yet. If you make a request for your data from another
website you'll get a CORS error. To fix this problem you should set the correct
CORS Headers. An easy fix for this is to create a file netlify.toml
in your
folder my-anker-data/
inside put the following:
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Methods = "*"
Access-Control-Allow-Headers = "*"
This will allow all origins to make all request for every file in this project. You can customize this for the level of security you want. Alternatively, you can use Netlify functions. As mentioned above you can also put your profile picture in your folder and add the link to your JSON file like following:
{
"name": "John Doe",
"profilePic": "<netlify-subdomain>.netlify.app/profile.jpg",
"links": "links...",
"social": "socials..."
}
.
├── app
│  ├── Controller.js
│  ├── Model.js
│  └── View.js
├── config.js
├── helpers.js
├── plugins
│  ├── Apps.js
│  └── FreeFall.js
└── themes
├── DefaultThemeBasic.js
└── DefaultThemePro.js
Inside src/js/
there are three directories app
, plugins
and themes
. The
themes
directory is the one where the themes are. There are two default themes
included in anker-app
repository.
The DefaultThemeBasic
is the one with very basic markup. The Pro version of
the theme can do more that just displaying links. In the Pro version you can add
dropdown links and third party apps e.g embedding YouTube video, showing your Patreon
button or mailing list form etc.
The markup for third party apps can be added in the Apps.js
file inside
plugins
directory.
class DefaultThemeBasic {
_generateHeaderMarkup(data) {
return `
<header class="header">
<div class="header__image-box">
<img id="header__image" class="header__image" src="${data.profilePic}" alt="Profile Pic">
</div>
<div class="header__profile-username">
<p class="header__username lead">${data.name}</p>
</div>
</header>
`;
}
_generateBodyMarkup(data) {
return `
<section class="app-body">
<ul class="links">
${data.links.map(this.#generateLinksMarkup).join('')}
</ul>
<ul class="social-links">
${data.socials.map(this.#generateSocialsMarkup).join('')}
</ul>
</section>
`;
}
_generateFooterMarkup() {
return `
<footer class="footer">
<div class="footer__logo-box">
<svg width="0" height="0" class="hidden">
<symbol version="1.1" id="logo" xmlns="http://www.w3.org/2000/svg" xmlnsSvg="http://www.w3.org/2000/svg" viewBox="0 0 21.62109 32.271481">
<g inkscapeLabel="Layer 1" inkscapeGroupmode="layer" id="layer1" transform="translate(-94.189455,-132.36426)">
<g id="g1427" transform="translate(-438.64257,-4.54199)">
<path d="m 543.64258,141.74805 c -5.95526,0 -10.81055,4.85529 -10.81055,10.81054 0,5.95526 4.85529,10.8125 10.81055,10.8125 5.95525,0 10.81054,-4.85724 10.81054,-10.8125 0,-5.95525 -4.85529,-10.81054 -10.81054,-10.81054 z m 0,2.64453 c 4.52534,0 8.16601,3.64067 8.16601,8.16601 0,4.52535 -3.64067,8.16602 -8.16601,8.16602 -4.52534,0 -8.16602,-3.64067 -8.16602,-8.16602 0,-4.52534 3.64068,-8.16601 8.16602,-8.16601 z" id="circle1358"></path>
<path d="m 536.64453,163.9707 a 1.322915,1.322915 0 0 0 -1.67969,0.82032 1.322915,1.322915 0 0 0 0.82032,1.68164 l 7.85742,2.70507 7.85937,-2.70507 a 1.322915,1.322915 0 0 0 0.82032,-1.68164 1.322915,1.322915 0 0 0 -1.68165,-0.82032 l -6.99804,2.40821 z" id="path1360"></path>
<path d="m 536.18555,136.90625 a 1.322915,1.322915 0 0 0 -1.32227,1.32227 1.322915,1.322915 0 0 0 1.32227,1.32421 h 14.91406 a 1.322915,1.322915 0 0 0 1.32226,-1.32421 1.322915,1.322915 0 0 0 -1.32226,-1.32227 z" id="path1362"></path>
</g>
</g>
</symbol>
</svg>
<svg class="logo"><use href="#logo"/></svg>
</div>
</footer>
`;
}
#generateLinksMarkup(link) {
return `<li class="link-item"><a class="link" href="${link.link}">${link.title}</a></li>`;
}
#generateSocialsMarkup(social) {
return `<li class="social-link-item">
<a class="social-link" href="${social.link}">
<svg class="social-icon">
<use href="./assets/tabler-sprite.svg#tabler-brand-${social.title}"/>
</svg>
</a>
</li>`;
}
}
export default new DefaultThemeBasic();
As you can see above the theme files only contain the markup. You can customize this file however it suits you. See below what the Pro version of the theme contains.
import FreeFall from '../plugins/FreeFall';
import apps from '../plugins/Apps';
class DefaultThemePro {
_generateHeaderMarkup(data) {
return `
<header class="header">
<div class="header__image-box">
<img id="header__image" class="header__image" src="${data.profilePic}" alt="Profile Pic">
</div>
<div class="header__profile-username">
<p class="header__username lead">${data.name}</p>
</div>
</header>
`;
}
_generateBodyMarkup(data) {
return `
<section class="app-body">
<div class="links">
${data.links.map(this.#generateLinksMarkup).join('')}
</div>
<ul class="social-links">
${data.socials.map(this.#generateSocialsMarkup).join('')}
</ul>
</section>
`;
}
_generateFooterMarkup() {
return `
<footer class="footer">
<div class="footer__logo-box">
<svg width="0" height="0" class="hidden">
<symbol version="1.1" id="logo" xmlns="http://www.w3.org/2000/svg" xmlnsSvg="http://www.w3.org/2000/svg" viewBox="0 0 21.62109 32.271481">
<g inkscapeLabel="Layer 1" inkscapeGroupmode="layer" id="layer1" transform="translate(-94.189455,-132.36426)">
<g id="g1427" transform="translate(-438.64257,-4.54199)">
<path d="m 543.64258,141.74805 c -5.95526,0 -10.81055,4.85529 -10.81055,10.81054 0,5.95526 4.85529,10.8125 10.81055,10.8125 5.95525,0 10.81054,-4.85724 10.81054,-10.8125 0,-5.95525 -4.85529,-10.81054 -10.81054,-10.81054 z m 0,2.64453 c 4.52534,0 8.16601,3.64067 8.16601,8.16601 0,4.52535 -3.64067,8.16602 -8.16601,8.16602 -4.52534,0 -8.16602,-3.64067 -8.16602,-8.16602 0,-4.52534 3.64068,-8.16601 8.16602,-8.16601 z" id="circle1358"></path>
<path d="m 536.64453,163.9707 a 1.322915,1.322915 0 0 0 -1.67969,0.82032 1.322915,1.322915 0 0 0 0.82032,1.68164 l 7.85742,2.70507 7.85937,-2.70507 a 1.322915,1.322915 0 0 0 0.82032,-1.68164 1.322915,1.322915 0 0 0 -1.68165,-0.82032 l -6.99804,2.40821 z" id="path1360"></path>
<path d="m 536.18555,136.90625 a 1.322915,1.322915 0 0 0 -1.32227,1.32227 1.322915,1.322915 0 0 0 1.32227,1.32421 h 14.91406 a 1.322915,1.322915 0 0 0 1.32226,-1.32421 1.322915,1.322915 0 0 0 -1.32226,-1.32227 z" id="path1362"></path>
</g>
</g>
</symbol>
</svg>
<svg class="logo"><use href="#logo"/></svg>
</div>
</footer>
`;
}
#generateLinksMarkup(link) {
let markup;
if (link.hasOwnProperty('type')) {
markup = `
<div class="link-item">
<div class="link-item__container">
<div class="link__app" data-app-id="${link.app}">
<div class="link__app-container">
<div class="link__app-content"></div>
<div class="link__app-close-btn">
<span class="link__app-close-icon-box" data-app-close="${link.app}">
<svg class="tabler-icon link__app-close-icon">
<use href="./assets/tabler-sprite.svg#tabler-x"/>
</svg>
</span>
</div>
</div>
</div>
<div class="link-item__btn-container">
<button class="link-item__button link" data-type="${link.type}" data-app-trigger="${link.app}">
<span class="link__text">${link.title}</span>
<span><svg class="tabler-icon link__icon">
<use href="./assets/tabler-sprite.svg#tabler-chevron-down"/>
</svg></span>
</button>
</div>
</div>
</div>
`
} else {
markup = `
<div class="link-item">
<a class="link" href="${link.link}">${link.title}</a>
</div>
`
}
return markup;
}
#generateSocialsMarkup(social) {
return `<li class="social-link-item">
<a class="social-link" href="${social.link}">
<svg class="tabler-icon social-icon">
<use href="./assets/tabler-sprite.svg#tabler-brand-${social.title}"/>
</svg>
</a>
</li>`;
}
_initScripts(data) {
// Scale link on hover
const links = document.querySelectorAll('.link');
links.forEach(link => {
link.addEventListener('mouseenter', (event) => {
event.target.style.transform = 'scale(1.05)';
event.target.style.transition = 'all .2s';
})
link.addEventListener('mouseleave', (event) => {
event.target.style.transform = 'scale(1)';
})
});
// Dropdown and Apps setup using FreeFall and Apps plugins
const freeFall = new FreeFall({
showDropdownCSS: 'link__app-show',
dataAttributes: {
trigger: 'data-app-trigger',
closer: 'data-app-close',
id: 'data-app-id'
}
});
// Extract apps related data from data object
const embedLinks = new Map();
data.links.forEach((link) => {
if (link.hasOwnProperty('app')) {
embedLinks.set(`${link.app}`, `${link.link}`);
}
});
const ddApp = document.querySelector('[data-app-id="youtube"]');
ddApp.addEventListener('afterDrop', (event) => {
const targetApp = event.detail;
const targetElem = targetApp.querySelector('.link__app-content');
targetElem.innerHTML = apps._appYouTube(embedLinks.get('youtube'));
});
}
}
export default new DefaultThemeBasic();
The Pro version of the theme is not very different from the basic version but it's more customizable. One important thing to note is that all the method names have to be left as they are. You only have to change the content of the methods. You can't rename the methods to something else.
Let's break down everything to understand what's going on.
The first part is some imports
. Those plugins are related to the themes. The first
one is a plugin called FreeFall
which is responsible for dropdowns. The second
plugin is Apps
which holds markups for third party apps. You can also add your
own Markup to Apps.js
.
import FreeFall from '../plugins/FreeFall';
import apps from '../plugins/Apps';
The header, body and footer markup methods are self-explanatory and pretty
straight forward. What is however important are the map()
methods inbetween
markups. The map methods generate markup for example from methods such as
#generateLinksMarkup()
and #generateSocialsMarkup()
. See the example below.
_generateBodyMarkup(data) {
return `
<section class="app-body">
<div class="links">
${data.links.map(this.#generateLinksMarkup).join('')}
</div>
<ul class="social-links">
${data.socials.map(this.#generateSocialsMarkup).join('')}
</ul>
</section>
`;
}
#generateLinksMarkup(link) {
let markup;
if (link.hasOwnProperty('type')) {
markup = `
<div class="link-item">
<div class="link-item__container">
<div class="link__app" data-app-id="${link.app}">
<div class="link__app-container">
<div class="link__app-content"></div>
<div class="link__app-close-btn">
<span class="link__app-close-icon-box" data-app-close="${link.app}">
<svg class="tabler-icon link__app-close-icon">
<use href="./assets/tabler-sprite.svg#tabler-x"/>
</svg>
</span>
</div>
</div>
</div>
<div class="link-item__btn-container">
<button class="link-item__button link" data-type="${link.type}" data-app-trigger="${link.app}">
<span class="link__text">${link.title}</span>
<span><svg class="tabler-icon link__icon">
<use href="./assets/tabler-sprite.svg#tabler-chevron-down"/>
</svg></span>
</button>
</div>
</div>
</div>
`
} else {
markup = `
<div class="link-item">
<a class="link" href="${link.link}">${link.title}</a>
</div>
`
}
return markup;
}
As you can see above the links markup for Pro version is different and has much more
HTML than the basic version. The method checks if the data has a type
property with
which it determines if dropdown should be added. If the type
property exists then
it adds the dropdown for the app to load in.
#generateSocialsMarkup(social) {
return `<li class="social-link-item">
<a class="social-link" href="${social.link}">
<svg class="tabler-icon social-icon">
<use href="./assets/tabler-sprite.svg#tabler-brand-${social.title}"/>
</svg>
</a>
</li>`;
}
The markup for the social icons is very straight forward as well. It just loops
through the data and generates the li
elements for social icons.
_initScripts(data) {
// Scale link on hover
const links = document.querySelectorAll('.link');
links.forEach(link => {
link.addEventListener('mouseenter', (event) => {
event.target.style.transform = 'scale(1.05)';
event.target.style.transition = 'all .2s';
})
link.addEventListener('mouseleave', (event) => {
event.target.style.transform = 'scale(1)';
})
});
// Dropdown and Apps setup using FreeFall and Apps plugins
const freeFall = new FreeFall({
showDropdownCSS: 'link__app-show',
dataAttributes: {
trigger: 'data-app-trigger',
closer: 'data-app-close',
id: 'data-app-id'
}
});
// Extract apps related data from data object
const embedLinks = new Map();
data.links.forEach((link) => {
if (link.hasOwnProperty('app')) {
embedLinks.set(`${link.app}`, `${link.link}`);
}
});
const ddApp = document.querySelector('[data-app-id="youtube"]');
ddApp.addEventListener('afterDrop', (event) => {
const targetApp = event.detail;
const targetElem = targetApp.querySelector('.link__app-content');
targetElem.innerHTML = apps._appYouTube(embedLinks.get('youtube'));
});
}
_initScripts()
is a protected method that runs all the scripts related to a
specific theme. All your theme related scripts go inside this method.
How do you wanna deploy it is up to you. If you want to use Anker as your
landing page then by all means do that but if you already have a website and
wanna use Anker as a bio link to have a link like: example.com/bio
then
nothing is stopping you from that as well.
If you already have a website then I'd suggest you to merge the code with
your CSS and JS and that will do it. However if you want to use Anker on it's
own either you use your own domain or Netlify. I might have a better method
for you to only deploy your dist/
directory to live server.
I use Git Submodules for deployment. If you want to use that as well I wrote this Gist explaining exactly that. Check out Anker's Demo it uses Git Submodules for deployment.
First off, thanks a lot for taking the time to read this far and considering to contribute. If you want to work on this project and make themes for others to use for free then join this organisation.
- Anker is made by developers for developers. As a developer you're obliged to contribute to such projects... Just kidding 😄 it's your choice but if you want to contribute I'd really appreciate it.
- If you ever wanted to make your own bio link and didn't like other overrated and overpriced services then you might want to contribute and distribute your favorite theme for free for others to use.
- Maybe you have better ideas and think that Anker is not executed correctly and would want to write better codebase.
- If you're a beginner to web development you definitely want some light weight projects to work on to practice your skills and apply what you've learned.
- Contributing to Open Source projects is good for later when you start looking for job. You can show the recruiters your contribution and it'll win you some extra points. Read the Contributing Guidelines before contributing!