In the world of customer relationship management, HubSpot has emerged as a powerhouse for managing and automating customer interactions. However, when dealing with multilingual content and global audiences, it's crucial to employ tools that can identify the language of your customers effectively. One such tool is Operations Hub pro with this language detection Custom Coded Action, which can be seamlessly integrated into HubSpot workflows to enhance customer communication and content personalization.
Demo
How language detect works in HubSpot ?
Text_LanguageDetect is a PHP library renowned for its language detection capabilities. Although natively designed for PHP, you can harness its power within your HubSpot workflows using a custom code block in JavaScript. Let’s explore how this library can benefit your HubSpot environment.
Language Identification
The primary feature of This Custom Coded Action is its accurate language identification. In a CRM like HubSpot, where interactions occur in multiple languages, this Custom Coded Action can automatically categorize and sort text content based on language. This facilitates more targeted and relevant communication, ensuring that your customers receive information in their preferred language.
Personalized Customer Communication
HubSpot’s strength lies in personalizing customer interactions. By incorporating this Custom Coded Action you can take personalization to the next level. Create workflows that adapt based on detected language, sending emails, responses, or content in the language your customers prefer. This enhancement improves the overall customer experience, making it more engaging and satisfying.
Language-Specific Automation
Use this Custom Coded Action to trigger automation based on language. For instance, you can set up workflows that route customer inquiries to language-specific support teams. This ensures that your customers receive support and information in their preferred language, enhancing engagement and satisfaction.
Integration with HubSpot Workflows
Integrating this Custom Coded action into your HubSpot workflows is seamless using the custom code block feature. This integration empowers you to create dynamic, language-aware processes within your CRM.
This Custom Coded action is a powerful tool for enhancing your HubSpot CRM when dealing with multilingual content and global customer interactions. By accurately identifying languages, personalizing customer communication, automating language-specific processes, you can provide a more tailored and effective customer experience.
Deploying Language Detection Code in HubSpot
Follow these steps to integrate the language detection functionality into your CRM:
1. Create a Blank Workflow
Start by creating a new workflow in your HubSpot account. This workflow will be the foundation for implementing the language detection code.
2. Create a Trigger with a Filter
Within your workflow, establish a trigger with a filter to specify when language detection should occur. For instance, if you want to detect the language based on a description, you can set a filter such as « Description is known. » This filter will determine when this custom code will be executed.
3. Add a Custom Code Block
To integrate language detection into your workflow, you’ll need to use a custom code block. This is where you’ll include the JavaScript code responsible for language detection.
4. Include Properties in the Code
Within the custom code block, you should define the properties you want to analyze. For example, if you’re detecting language based on a product description, you might use a variable like textToAnalyze
to represent the description text.
5. Copy Code from the Repository
To make this integration even more straightforward, we’ve provided a repository with pre-compiled code for language detection. You can access this repository at github Detect-language.
Inside the repository, navigate to the dist
folder and copy the contents. These pre-compiled files contain the necessary code to execute language detection using this custom code.
6. Set Output Variables
In your custom code block, you need to specify the output variables and their types. For language detection, you typically want to capture the detected language and the confidence level. Define the following variables:
language
with the type ‘string’confidence
with the type ‘number’
This step ensures that the detected language and the confidence score are made available for use in your HubSpot workflow.
By following these steps, you’ll seamlessly integrate language detection into your HubSpot workflow, enabling you to automate language detection and enhance your CRM’s capabilities for personalized customer communication and content organization.
The source code
Do not copy this code, it’s the source code use the code in the /dist
folder of this repo : github Detect-language.
const LanguageDetect = require('languagedetect');
exports.main = async (event, callback) => {
const textToAnalyze = event.inputFields.textToAnalyze;
if (!textToAnalyze) throw new Error('textToAnalyze is not set, are you sure you put textToAnalyze in the "properties to include in code" ? ');
const lngDetector = new LanguageDetect();
const languageFound = lngDetector.detect(textToAnalyze,2);
if(languageFound.length === 0) throw new Error('We failed to indentify the language');
const [language, confidance]= languageFound[0];
if(!language) throw new Error('Error when identifing the language');
if(!confidance) throw new Error('Error when identifing the language no confidance data');
callback({
outputFields: {
language,
confidance
}
});
}
What is HubSpot-OperationHub-cca-compiler?
HubSpot-OperationHub-cca-compiler is a local development tool that provides a framework for working on your Custom Coded Actions (CCAs) locally and executing them in the same context as HubSpot. With this tool, you can write your CCAs using the libraries you want, even if they are not officially supported by HubSpot.
Demo
Key Features
Here are some of the key features and benefits of HubSpot-OperationHub-cca-compiler:
1. Library Freedom
Want to create a Custom Coded Action to perform a specific task, like validating phone number formats? Instead of writing all the logic from scratch, you can look for open-source modules and libraries online. However, Custom Coded Actions don’t allow you to import external libraries directly. HubSpot-OperationHub-cca-compiler offers a solution to this limitation. You can install the libraries you need locally, and the tool will package everything into a single file that works seamlessly within HubSpot.
2. Easy Setup
Using HubSpot-OperationHub-cca-compiler is straightforward. Start by cloning the project and installing the necessary dependencies with a simple command:
git clone https://github.com/Antoinebr/HubSpot-OperationHub-cca-compiler.git
cd HubSpot-OperationHub-cca-compiler
npm install
Create a .env
file with your private App Token, which should look like this:
privateAppToken = "your-private-app-token"
3. Project Initialization
To create a new Custom Coded Action project, you can use the following command:
npm run init <nameOfYourProject>
For example:
npm run init my-new-custom-coded-action
This command generates a template that contains a file named cca.js
. This is where you write your custom code.
4. Code Your CCA
Inside cca.js
, you can write your CCA logic. The template provides a clear structure for your code, making it easy to follow. Here’s an example of a CCA that formats phone numbers:
const format = require('@sturdynut/i18n-phone-formatter');
exports.main = async (event, callback) => {
const phoneNumber = event.inputFields.phoneNumber;
if (!phoneNumber) throw new Error('phoneNumber is not set, are you sure you put phoneNumber in the "properties to include in code"?');
const countryCode = event.inputFields.countryCode;
if (!countryCode) throw an Error('countryCode is not set, are you sure you put country in the "properties to include in code"?');
const formattedNumber = format.formatE164(countryCode, phoneNumber);
callback({
outputFields: {
formattedNumber
}
});
}
The event.js file represents the properties you can include in your code, making it easy to interact with the data.
5. Node Modules
You can install any node modules you require for your project. Install them with npm, and then use them within your code:
npm install @sturdynut/i18n-phone-formatter
And then add it to your code :
const format = require('@sturdynut/i18n-phone-formatter');
6. Build and Deploy
After writing your CCA, build the project by running the following command:
npm run build <nameOfTheFolder>
This command creates a dist/ folder inside your project, which contains the compiled code.
Unfortunately, there’s no automated deployment for the compiled code. You will need to manually copy and paste the contents of dist/index.js into the Custom Coded Action block in HubSpot.
Conclusion
HubSpot-OperationHub-cca-compiler is a valuable tool for anyone looking to overcome limitations in HubSpot’s Operations Hub. It empowers you to work with the libraries you need, providing greater flexibility in your custom coding projects. With an easy setup process and well-structured templates, this tool streamlines the development and deployment of Custom Coded Actions, enhancing your capabilities within HubSpot.
Concept
See how it works / and implement it
PeopleDataLabs API key
You need to get an API key on the PeopleDataLabs website’s.
You need to set the peopleDataLabsAPI
key in the secret, the API key has to be set in the secret section of the Custom Coded Action. Use the name peopleDataLabsAPI
Set the domain name variable
Set the variable name domainName
in the property to include in code.
Code
Paste the following code :
const axios = require('axios');
exports.main = async (event, callback) => {
if (!process.env.peopleDataLabsAPI) throw new Error('The peopleDataLabs API key has to be set in the secret section');
const domainName = event.inputFields.domainName;
if (!domainName) throw new Error('domainName is not set, are you sure you put domainName in the "properties to include in code" ? ');
const websiteInfos = await getWebsiteInfos(domainName).catch(axiosErrorHandler)
if (!websiteInfos.data) throw new Error(`We couldn't grab your websiteInfos infos`);
const {
name,
size,
employee_count,
id,
founded,
industry,
linkedin_id,
linkedin_url,
facebook_url,
twitter_url,
summary
} = websiteInfos.data;
callback({
outputFields: {
name,
size,
employee_count,
id,
founded,
industry,
linkedin_id,
linkedin_url,
facebook_url,
twitter_url,
summary
}
});
}
/**
* Retrieves information about a website's associated company using the PeopleDataLabs API.
*
* @async
* @function
* @param {string} domainName - The domain name of the website for which you want to fetch information.
* @throws {Error} Throws an error if domainName is not a string or if it is empty.
* @returns {Promise<Object>} A Promise that resolves to an object containing information about the company.
* @see {@link https://docs.peopledatalabs.com/|PeopleDataLabs API Documentation}
*/
const getWebsiteInfos = async (domainName) => {
if (typeof domainName !== 'string' || domainName.trim() === '') throw new Error('Invalid domainName parameter. It must be a non-empty string.');
// Construct the API endpoint URL with the provided domainName and API key.
const endpoint = `https://api.peopledatalabs.com/v5/company/enrich?api_key=${process.env.peopleDataLabsAPI}&pretty=True&website=${domainName}`;
// Make a GET request to the API and return the result.
return axios.get(endpoint);
}
/**
* Handles errors thrown by axios requests and logs relevant information.
*
* @param {Error} e - The error object thrown by axios.
*/
const axiosErrorHandler = e => {
console.log(e);
//console.log("The : ", e.config.method, " call on", e.config.url, "failed");
console.log("error code retuned ", e.code);
console.log("error data returned ", e.response.data);
}
Set the output / to use the data in the WorkFlow
name,
size,
employee_count,
id,
founded,
industry,
linkedin_id,
linkedin_url,
facebook_url,
twitter_url,
summary
Like so :
Then don’t forget to finish the logic with copy property values blocks to update the company record.
The logic / to find the firstname and gender
The idea is to split an email by a delimiter if one exists, then search in a huge first names list for a match.
Demo / see the logic in action
Take a look at this video to see the logic in action
Implement / in your portal
Follow this video to implement this Custom Coded Action
The code / ready to be used
Here’s where you will find the code :
The source / code
Here’s the logic I created, but do not use this code in the Custom Coded Action ! Use the link above.
( This code is just the source code requires a extra build step ( Webpack ) ) this is why you can’t use that code directly.
const firstnames = require('./firstnames.js');
exports.main = async (event, callback) => {
const email = event.inputFields.email;
if (!email) throw new Error('email is not set, are you sure you put email in the "properties to include in code" ? ');
const emailWithoutDomain = email.split('@')[0];
const firstNameAndLastName = splitString(emailWithoutDomain);
if(firstNameAndLastName.length === 1) throw new Error(`${email} can't be splitted by a separator`);
const chunks = splitString(emailWithoutDomain).map(chunk => capitalizeString(chunk));
const firstNameFound = [];
for (const chunk of chunks) {
if (firstnames[chunk]) {
firstNameFound.push({
firstName: chunk,
gender: firstnames[chunk].gender
});
}
}
if(firstNameFound.length === 0) throw new Error('The firstName was not in the database');
callback({
outputFields: {
gender: firstNameFound.length > -1 ? firstNameFound[0].gender : "N/A",
firstName: firstNameFound.length > -1 ? firstNameFound[0].firstName : "N/A"
}
});
}
/**
* Splits a string into an array of parts based on delimiters such as commas, dashes, underscores, dots, and forward slashes.
*
* @param {string} str - The string to be split.
* @returns {array} - An array of the split string parts.
*/
const splitString = (str) => {
return str.split(/[-_.]+/);
}
exports.splitString = splitString;
/**
* Capitalizes the first character of a string.
*
* @param {string} str - The string to capitalize.
* @returns {string} - The capitalized string.
*/
const capitalizeString = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
}
exports.capitalizeString = capitalizeString;
The logic we want to create
As you can see the deal is renamed based on the existing deals
Prerequisites
- HubSpot and Operations Hub
- Basic JavaScript knowledge
- Node.js installed on your computer ( if you want to follow along the coding part )
- An API key for your portal ( private App token )
The logic
- In a WorkFlow for every new deals which do not contain a given separator here
-RQ
- First we ask the HubSpot API the latest deal containing
-RQ
- Split the name by the separator to get the numeric value after the separator
- Add one to this value
- Build the new deal name and return it to the Workflow
How to / tutorial
In this video I explain the concept, then I show you how to code the logic and how to implement it.
The code
Here’s the final code which you can implement in your portal :
/***
* SET THE SEPARTOR
* Here set the separator
* If your deals name are Deal-RQ001 then set "-RQ" as a sepator
* The code will read the latest deal
*/
const separator = "-RQ";
const axios = require('axios');
const axiosConfig = {
headers: {
authorization: `Bearer ${process.env.privateAppToken}`
}
};
exports.main = async (event, callback) => {
if (!separator || separator === "") throw new Error('separator is not set, are you sure you put separator in the code ? ( see first lines )');
const dealName = event.inputFields.dealName;
if (!dealName) throw new Error('dealName is not set, are you sure you put dealName in the "properties to include in code" ? ');
const deals = await searchDealsWithSeparator(separator).catch(axiosErrorHandler);
console.log(deals.data.results[0].properties.dealname);
const latestDealName = deals.data.results[0].properties.dealname;
if (!latestDealName.includes(separator)) throw new Error("The deal doesn't contain the right separator")
const numericValueAfterSeparator = latestDealName.split(separator)[1]
const newNumericValue = parseInt(numericValueAfterSeparator) + 1;
callback({
outputFields: {
newDealName: `${dealName}${separator}${newNumericValue}`
}
});
}
const getLastestsDeals = async () => {
const endpoint = `https://api.hubapi.com/deals/v1/deal/recent/`;
return axios.get(endpoint, axiosConfig);
}
const searchDealsWithSeparator = async (separator) => {
const endpoint = `https://api.hubapi.com/crm/v3/objects/deals/search`;
return axios.post(endpoint, {
"filterGroups": [{
"filters": [{
"value": `*${separator}*`,
"propertyName": "dealname",
"operator": "CONTAINS_TOKEN"
}]
}],
"properties": [
"hubspot_owner_id",
"dealname",
"name"
],
"sorts": [{
"propertyName": "id",
"direction": "DESCENDING"
}],
"limit": 1,
//"after": after
}, axiosConfig);
}
/**
* Handles errors thrown by axios requests and logs relevant information.
*
* @param {Error} e - The error object thrown by axios.
*/
const axiosErrorHandler = e => {
console.log("The : ", e.config.method, " call on", e.config.url, "failed");
console.log("error code retuned ", e.code);
console.log("error data returned ", e.response.data);
}
Enrichir son CRM HubSpot en se basant sur le SIRET
Le registre du commerce à beaucoup d’informations sur vos clients, le SIRET peut vous aider à les récupérer et enrichir votre CRM.
Depuis le numéro SIRET, vous pouvez notamment récupérer la raison sociale exacte d’une entreprise son effectif ou encore son adresse postale.
Concept et démonstration
Concept de l’appel à une API via Operations Hub
Le principe est assez simple, un WorkFlow utilisera un block Custom Code pour envoyer une requête à l’API de l’INSEE pour demander des infos.
Si l’API de l’INSEE renvoie les informations, alors nous les sauvegardons dans l’objet entreprise. Rien de plus simple.
Démonstration de l’intégration
Mieux qu’un long discours, j’ai enregistré une courte vidéo qui montre cette intégration en fonctionnement. Dans cette video je vais montrer les informations que renvoie l’API à HubSpot.
Pourquoi ne peut on pas trouver le numéro SIRET d’une entreprise ?
En fait l’API SIREN le propose, mais le soucis se trouve sur la clé de recherche.Je m’explique, si vous cherchez un numéro de SIRET en se basant sur nom de l’entreprise, vous allez avoir énormement de réponses… Et que faire avec 25, 30… SIRET, comment décider celui qui correspond à l’entreprise que vous avez dans le CRM ?.. C’est compliqué, il faudrait rafiner la data.
De ce fait je n’aborderais pas ce point dans l’article.
Prérequis
Pour mettre en place cette intégration, il vous faudra Operations Hub pro, car nous allons utiliser le block custom code. Vous allez aussi avoir besoin de créer un compte sur le site de l’INSEE pour récupérer un accès à l’API.
Il va aussi vous fallour une propriété SIREN dans vos entrerprises ou contacts
Créer un compte api.insee.fr
Il vous faut un compte INSEE, rendez-vous sur cette URL pour ce faire.
Cliquez sur créer son compte
Remplissez le formulaire :
Souscrivez à l’API SIREN
Générez un jeton d’accès
J’ai mis en place un jeton d’accès qui n’expire jamais en metant la valeur la plus élevée possible, j’ai mis : 3875820019684211
secondes.
Mettre en place le workflow
Créez un WorkFlow qui se base sur entreprise ou autre.
Le trigger du WorkFlow
Installez le custom code
Séléctionnez le language
Ici notre Custom Coded Action est écrite en JavaScript il vous faut donc séléctionner Node.js comme language.
Insérez votre jetons d’accès comme secret
Nommez votre secret apiInseeToken
attention de en pas rajouter ou d’enlever des majuscules ou des espaces.
Ajoutez une variable SIRET
La variable siret
correspond à la porpriété qui cotient le numéro SIRET.
Ajoutez-la comme ce-ci :
Collez le code suivant :
const axios = require('axios');
const axiosConfig = {
headers: {
authorization: `Bearer ${process.env.apiInseeToken}`
}
};
exports.main = async (event, callback) => {
const siret = event.inputFields.siret;
if (!siret) throw new Error('siret is not set, are you sure you put siret in the "properties to include in code" ? ');
const siretInfos = await getInfosFromSiret(siret).catch(axiosErrorHandler)
if (!siretInfos.data) throw new Error(`We couldn't grab your siretInfos`);
/**
*
* Etablissement DATA
*
**/
const { etablissement } = siretInfos.data;
if (!etablissement) throw new Error(`Couldn't get the etablissement data`);
const {
siren,
nic,
statutDiffusionEtablissement,
dateCreationEtablissement,
trancheEffectifsEtablissement,
anneeEffectifsEtablissement,
activitePrincipaleRegistreMetiersEtablissement,
dateDernierTraitementEtablissement,
etablissementSiege,
nombrePeriodesEtablissement,
} = etablissement;
/**
*
* Etablissement Unite Legale
*
**/
let etatAdministratifUniteLegale = null;
let statutDiffusionUniteLegale = null;
let dateCreationUniteLegale = null;
let categorieJuridiqueUniteLegale = null;
let denominationUniteLegale = null;
let sigleUniteLegale = null;
let denominationUsuelle1UniteLegale = null;
let denominationUsuelle2UniteLegale = null;
let denominationUsuelle3UniteLegale = null;
let sexeUniteLegale = null;
let nomUniteLegale = null;
let nomUsageUniteLegale = null;
let prenom1UniteLegale = null;
let prenom2UniteLegale = null;
let prenom3UniteLegale = null;
let prenom4UniteLegale = null;
let prenomUsuelUniteLegale = null;
let pseudonymeUniteLegale = null;
let activitePrincipaleUniteLegale = null;
let nomenclatureActivitePrincipaleUniteLegale = null;
let identifiantAssociationUniteLegale = null;
let economieSocialeSolidaireUniteLegale = null;
let societeMissionUniteLegale = null;
let caractereEmployeurUniteLegale = null;
let trancheEffectifsUniteLegale = null;
let anneeEffectifsUniteLegale = null;
let nicSiegeUniteLegale = null;
let dateDernierTraitementUniteLegale = null;
let anneeCategorieEntreprise = null;
if (typeof etablissement.uniteLegale !== "undefined") {
etatAdministratifUniteLegale = etablissement.uniteLegale.etatAdministratifUniteLegale;
statutDiffusionUniteLegale = etablissement.uniteLegale.statutDiffusionUniteLegale;
dateCreationUniteLegale = etablissement.uniteLegale.dateCreationUniteLegale;
categorieJuridiqueUniteLegale = etablissement.uniteLegale.categorieJuridiqueUniteLegale;
denominationUniteLegale = etablissement.uniteLegale.denominationUniteLegale;
sigleUniteLegale = etablissement.uniteLegale.sigleUniteLegale;
denominationUsuelle1UniteLegale = etablissement.uniteLegale.denominationUsuelle1UniteLegale;
denominationUsuelle2UniteLegale = etablissement.uniteLegale.denominationUsuelle2UniteLegale;
denominationUsuelle3UniteLegale = etablissement.uniteLegale.denominationUsuelle3UniteLegale;
sexeUniteLegale = etablissement.uniteLegale.sexeUniteLegale;
nomUniteLegale = etablissement.uniteLegale.nomUniteLegale;
nomUsageUniteLegale = etablissement.uniteLegale.nomUsageUniteLegale;
prenom1UniteLegale = etablissement.uniteLegale.prenom1UniteLegale;
prenom2UniteLegale = etablissement.uniteLegale.prenom2UniteLegale;
prenom3UniteLegale = etablissement.uniteLegale.prenom3UniteLegale;
prenom4UniteLegale = etablissement.uniteLegale.prenom4UniteLegale;
prenomUsuelUniteLegale = etablissement.uniteLegale.prenomUsuelUniteLegale;
pseudonymeUniteLegale = etablissement.uniteLegale.pseudonymeUniteLegale;
activitePrincipaleUniteLegale = etablissement.uniteLegale.activitePrincipaleUniteLegale;
nomenclatureActivitePrincipaleUniteLegale = etablissement.uniteLegale.nomenclatureActivitePrincipaleUniteLegale;
identifiantAssociationUniteLegale = etablissement.uniteLegale.identifiantAssociationUniteLegale;
economieSocialeSolidaireUniteLegale = etablissement.uniteLegale.economieSocialeSolidaireUniteLegale;
societeMissionUniteLegale = etablissement.uniteLegale.societeMissionUniteLegale;
caractereEmployeurUniteLegale = etablissement.uniteLegale.caractereEmployeurUniteLegale;
trancheEffectifsUniteLegale = etablissement.uniteLegale.trancheEffectifsUniteLegale;
anneeEffectifsUniteLegale = etablissement.uniteLegale.anneeEffectifsUniteLegale;
nicSiegeUniteLegale = etablissement.uniteLegale.nicSiegeUniteLegale;
dateDernierTraitementUniteLegale = etablissement.uniteLegale.dateDernierTraitementUniteLegale;
anneeCategorieEntreprise = etablissement.uniteLegale.anneeCategorieEntreprise;
}
/*
*
* Adresse Etablissement
*
*/
let complementAdresseEtablissement = null;
let numeroVoieEtablissement = null;
let indiceRepetitionEtablissement = null;
let typeVoieEtablissement = null;
let libelleVoieEtablissement = null;
let codePostalEtablissement = null;
let libelleCommuneEtablissement = null;
let libelleCommuneEtrangerEtablissement = null;
let distributionSpecialeEtablissement = null;
let codeCommuneEtablissement = null;
let codeCedexEtablissement = null;
let libelleCedexEtablissement = null;
let codePaysEtrangerEtablissement = null;
let libellePaysEtrangerEtablissement = null;
if (typeof etablissement.adresseEtablissement !== "undefined") {
complementAdresseEtablissement = etablissement.adresseEtablissement.complementAdresseEtablissement;
numeroVoieEtablissement = etablissement.adresseEtablissement.numeroVoieEtablissement;
indiceRepetitionEtablissement = etablissement.adresseEtablissement.indiceRepetitionEtablissement;
typeVoieEtablissement = etablissement.adresseEtablissement.typeVoieEtablissement;
libelleVoieEtablissement = etablissement.adresseEtablissement.libelleVoieEtablissement;
codePostalEtablissement = etablissement.adresseEtablissement.codePostalEtablissement;
libelleCommuneEtablissement = etablissement.adresseEtablissement.libelleCommuneEtablissement;
libelleCommuneEtrangerEtablissement = etablissement.adresseEtablissement.libelleCommuneEtrangerEtablissement;
distributionSpecialeEtablissement = etablissement.adresseEtablissement.distributionSpecialeEtablissement;
codeCommuneEtablissement = etablissement.adresseEtablissement.codeCommuneEtablissement;
codeCedexEtablissement = etablissement.adresseEtablissement.codeCedexEtablissement;
libelleCedexEtablissement = etablissement.adresseEtablissement.libelleCedexEtablissement;
codePaysEtrangerEtablissement = etablissement.adresseEtablissement.codePaysEtrangerEtablissement;
libellePaysEtrangerEtablissement = etablissement.adresseEtablissement.libellePaysEtrangerEtablissement;
}
/*
*
* Adresse Etablissement 2
*
*/
let complementAdresse2Etablissement = null;
let numeroVoie2Etablissement = null;
let indiceRepetition2Etablissement = null;
let typeVoie2Etablissement = null;
let libelleVoie2Etablissement = null;
let codePostal2Etablissement = null;
let libelleCommune2Etablissement = null;
let libelleCommuneEtranger2Etablissement = null;
let distributionSpeciale2Etablissement = null;
let codeCommune2Etablissement = null;
let codeCedex2Etablissement = null;
let libelleCedex2Etablissement = null;
let codePaysEtranger2Etablissement = null;
let libellePaysEtranger2Etablissement = null;
if (typeof etablissement.adresse2Etablissement !== "undefined") {
complementAdresse2Etablissement = etablissement.adresse2Etablissement.complementAdresse2Etablissement;
numeroVoie2Etablissement = etablissement.adresse2Etablissement.numeroVoie2Etablissement;
indiceRepetition2Etablissement = etablissement.adresse2Etablissement.indiceRepetition2Etablissement;
typeVoie2Etablissement = etablissement.adresse2Etablissement.typeVoie2Etablissement;
libelleVoie2Etablissement = etablissement.adresse2Etablissement.libelleVoie2Etablissement;
codePostal2Etablissement = etablissement.adresse2Etablissement.codePostal2Etablissement;
libelleCommune2Etablissement = etablissement.adresse2Etablissement.libelleCommune2Etablissement;
libelleCommuneEtranger2Etablissement = etablissement.adresse2Etablissement.libelleCommuneEtranger2Etablissement;
distributionSpeciale2Etablissement = etablissement.adresse2Etablissement.distributionSpeciale2Etablissement;
codeCommune2Etablissement = etablissement.adresse2Etablissement.codeCommune2Etablissement;
codeCedex2Etablissement = etablissement.adresse2Etablissement.codeCedex2Etablissement;
libelleCedex2Etablissement = etablissement.adresse2Etablissement.libelleCedex2Etablissement;
codePaysEtranger2Etablissement = etablissement.adresse2Etablissement.codePaysEtranger2Etablissement;
libellePaysEtranger2Etablissement = etablissement.adresse2Etablissement.libellePaysEtranger2Etablissement;
}
callback({
outputFields: {
siren,
nic,
siret,
statutDiffusionEtablissement,
dateCreationEtablissement,
trancheEffectifsEtablissement,
anneeEffectifsEtablissement,
activitePrincipaleRegistreMetiersEtablissement,
dateDernierTraitementEtablissement,
etablissementSiege,
nombrePeriodesEtablissement,
etatAdministratifUniteLegale,
statutDiffusionUniteLegale,
dateCreationUniteLegale,
categorieJuridiqueUniteLegale,
denominationUniteLegale,
sigleUniteLegale,
denominationUsuelle1UniteLegale,
denominationUsuelle2UniteLegale,
denominationUsuelle3UniteLegale,
sexeUniteLegale,
nomUniteLegale,
nomUsageUniteLegale,
prenom1UniteLegale,
prenom2UniteLegale,
prenom3UniteLegale,
prenom4UniteLegale,
prenomUsuelUniteLegale,
pseudonymeUniteLegale,
activitePrincipaleUniteLegale,
nomenclatureActivitePrincipaleUniteLegale,
identifiantAssociationUniteLegale,
economieSocialeSolidaireUniteLegale,
societeMissionUniteLegale,
caractereEmployeurUniteLegale,
trancheEffectifsUniteLegale,
anneeEffectifsUniteLegale,
nicSiegeUniteLegale,
dateDernierTraitementUniteLegale,
anneeCategorieEntreprise,
complementAdresseEtablissement,
numeroVoieEtablissement,
indiceRepetitionEtablissement,
typeVoieEtablissement,
libelleVoieEtablissement,
codePostalEtablissement,
libelleCommuneEtablissement,
libelleCommuneEtrangerEtablissement,
distributionSpecialeEtablissement,
codeCommuneEtablissement,
codeCedexEtablissement,
libelleCedexEtablissement,
codePaysEtrangerEtablissement,
libellePaysEtrangerEtablissement,
complementAdresse2Etablissement,
numeroVoie2Etablissement,
indiceRepetition2Etablissement,
typeVoie2Etablissement,
libelleVoie2Etablissement,
codePostal2Etablissement,
libelleCommune2Etablissement,
libelleCommuneEtranger2Etablissement,
distributionSpeciale2Etablissement,
codeCommune2Etablissement,
codeCedex2Etablissement,
libelleCedex2Etablissement,
codePaysEtranger2Etablissement,
libellePaysEtranger2Etablissement
}
});
}
const getInfosFromSiret = async (siret) => {
if (!siret || siret === "") throw new Error('Siret is missing');
const endpoint = `https://api.insee.fr/entreprises/sirene/V3/siret/${siret}`;
return axios.get(endpoint, axiosConfig);
}
const searchSiret = async (query) => {
const endpoint = `https://api.insee.fr/entreprises/sirene/V3/siren?q=periode(denominationUniteLegale:${query})`;
return axios.get(endpoint, axiosConfig);
}
/**
* Handles errors thrown by axios requests and logs relevant information.
*
* @param {Error} e - The error object thrown by axios.
*/
const axiosErrorHandler = e => {
console.log("The : ", e.config.method, " call on", e.config.url, "failed");
console.log("error code retuned ", e.code);
console.log("error data returned ", e.response.data);
}
Configurez la sortie ( output function )
Sauvegarder le resultat dans le CRM
Pour sauvegarder les infos dans la fiche entreprise il suffit d’utiliser le block copy property value
rien de plus simple. Il suffit de deux clics pour convertir ce que l’on a configuré dan le « data output » en propriété du CRM.
Sentiment analysis demo
The fastest way to understand what you can achieve with this automation is to watch this short video.
The concept / schema
The concept is pretty simple, first we get the message from a user, then we create a Workflow which will listen new tickets.
This Workflow sends the content of the message to an API, then the API returns the sentiment.
Then we save the sentiment in the ticket.
How to set it up
In this part, I will explain how to create the workflow.
The Workflow trigger
Here’s the Workflow trigger we use is ticket description is known
, so when we have a message to analyze.
Send the message to the API
Register to the API and grab an API key
We’re going to use the API Layer sentiment analysis API.
First go on this link and register, then find the Sentiment Analysis API
Take the free plan as you can analyze up to 1000 messages per month.
Then Get the API key :
Set up the code in the custom coded action
To send the message to the API, we use the custom code block, so create one.
For the language pick Node.js
:
Then add a secret with the name : apiLayerKey
, so click on add secret
and paste the API key you got from API layer as the value.
Then add the message you want to analyze here, in my example I would like to analyze the Ticket description
so in the section property to include in code
I pick the ticket property Ticket description
On the left of the dropdown we have to name our property, put the name : textToAnalyze
do not change the casing and do not add extra white spaces.
Paste the code :
In the code section remove everything then paste the following code :
const axios = require('axios');
const axiosConfig = {
headers: {
apikey: process.env.apiLayerKey
}
};
exports.main = async (event, callback) => {
const textToAnalyze = event.inputFields.textToAnalyze;
if (!textToAnalyze) throw new Error('textToAnalyze is not set, are you sure you put textToAnalyze in the "properties to include in code" ? ');
const analysis = await analyzeSentiment(textToAnalyze).catch(axiosErrorHandler)
if (!analysis.data) throw new Error(`We couldn't grab the analysis infos`);
const { sentiment, language, confidence } = analysis.data;
callback({
outputFields: {
sentiment,
language,
confidence
}
});
}
const analyzeSentiment = body => {
if (!body) throw new Error(`There's no body`);
const endpoint = `https://api.apilayer.com/sentiment/analysis`;
return axios.post(endpoint, body, axiosConfig);
}
/**
* Handles errors thrown by axios requests and logs relevant information.
*
* @param {Error} e - The error object thrown by axios.
*/
const axiosErrorHandler = e => {
console.log("The : ", e.config.method, " call on", e.config.url, "failed");
console.log("error code retuned ", e.code);
console.log("error data returned ", e.response.data);
}
Add the output
The Custom Coded Action outputs’ is the data returned by the block to the workflow. In other words, the data other workflow blocks will be capable to use.
So in the output section add the following with the appropriate types (the order doesn’t matter)
string : sentiment
string : language
number : confidence
Test the logic
Now you can save the block, before that you can also give it a try by analyzing one of the existing ticket
Save the result in the ticket
To save the result in the ticket, nothing easier, just use the copy property value
block.
Use the sentiment value for the automation
Starting from here, you have the sentiment in your workflow, a so lot of automation can be run based on this. Why not sending an internal message to the contact owner ? This is the power of the workflows, so the automations are up to you.
Write your first Custom Coded Action
In this article I’m will guide you and how to create your first Custom Coded Action. If you are not a developer this article is for you. I will explain without too much sideways how to create your first Custom Coded Action.
Let’s get started
Anatomy of a Custom Coded Action
First create a Workflow in my example I created one based on contact.
Then click on the plus sign to add a new block.
Select the Custom Code block
The code
Let’s take a look at the default code provided by HubSpot :
Variables
To use a custom property in your code, you have to select it in the « property to include in code » section.
Here I selected two contact properties : Email
and carPlate
to select the properties you want to use in your code click on the dropdown.
Then on the fields on the left you can set the variable name you want. Here I ketp the default names.
Then to use it in your code you have to use the following notation :
To access the email address in your code use :
const email = event.inputFields['email'];
Now, the contact’s email address will be set in the email variable.
The const email =
parts means we store the email address in the email
variable
Alternatively, you can use the dot notation like so :
const email = event.inputFields.email;
The keyword const
is used most of the time to declare a variable, but you can also use the let
keyword.
Like so :
let email = event.inputFields.email
The difference between const
and let
is : const
cannot be reassigned, but let
can.
Example :
This doesn’t work :
const email = event.inputFields.email:
email = "antoine@gmail.com";
But if you use let
instead it will work
let email = event.inputFields.email:
email = "antoine@gmail.com";
In a nutshell:
« const » is for things that won’t change – once you put something in that box, it stays that way. « let » is for things that might change – you can put something in the box and later replace it with something else.
So, when you’re picking between « const » and « let, » think about whether you want a locked box that won’t change or a flexible box that you can update whenever you need to.
Display / return value from our code
To display something from our code you can use console.log()
which is a function.
Think of console.log like a talking mirror that helps you understand what’s happening inside your program:
- Talking Mirror (console.log): Imagine you have a magical mirror that can talk. Whenever you want to know what’s going on inside a room, you ask the mirror to tell you. It gives you information about what’s happening without affecting the room itself.
-
Your Message (Value to Log): To understand what’s happening, you tell the mirror something specific. It could be anything – a thought, a question, or just a random observation.
-
Mirror’s Response (Output): The mirror takes your message and speaks it out loud. It tells you exactly what you asked for. This doesn’t change anything inside the room; it just gives you information.
// The talking mirror (console.log)
let message = "Hello, magical mirror!";
console.log(message); // The mirror responds by showing the message
let number = 42;
console.log("The answer to everything is:", number); // The mirror responds again
So in our Custom Coded Action if I want to display the email address or / and the car plate my code will look like this :
Then if I click Test
The result will look like this :
Here’s the code you can use to test this
exports.main = async (event, callback) => {
const email = event.inputFields.email;
console.log("Email is ", email);
callback({
outputFields: {
email: email
}
});
}
Return data to the workflow
But this only display the value in the Logs section, if you want to return the data to the WorkFlow and use that date in an other block. You have to use the callBack function.
This is useful if for example to save the result of the Custom Coded Action in the contact
To return the data in the WorkFlow you have to use the callback
callback({
outputFields: {
email: email
}
});
This part of the code will return the email value to the WorkFlow, then it will be accessible to other blocks.
- 1 – Write
email : email
inside the{ }
of the outputfields - 2 – Add the value in the data outputs section
Like so :
If you click save in the top right corner, now the email variable will be accessible to other blocks.
I use the set property value
block and can select the email from the Custom Coded Action.
IF statements
An « if statement » in JavaScript is like a decision-making tool in your everyday life. Imagine you’re deciding whether to go outside or stay indoors based on the weather. The « if statement » in JavaScript helps your program make similar decisions based on certain conditions.
Here’s a simple way to understand it:
Imagine you’re a robot programmed to make decisions based on weather conditions:
If it’s sunny: You’ll decide to go for a walk outside.
If it’s rainy: You’ll decide to stay indoors and read a book
In JavaScript, an « if statement » works similarly:
let weather = "sunny"; // You can change this value to "rainy" or something else
let activity;
if (weather === "sunny") {
activity = "go for a walk";
} else if (weather === "rainy") {
activity = "stay indoors and read a book";
} else {
activity = "not sure what to do";
}
console.log("Today's weather is " + weather + ". I will " + activity + ".");
Functions
A function in JavaScript is similar to the functions you use in Excel or Google spreadsheet.
As an example, imagine you have a « Greet » machine that takes a name as input and gives you a greeting as output:
- Greet Machine (Function): You have a machine with a big button labeled « Greet. » When you press the button, it asks for a name, and then it prints out a greeting.
-
Your Friend’s Name (Parameter): Before you press the button, you know your friend’s name. You’ll tell the machine this name so it can use it to greet your friend properly.
-
Greeting Message (Function Body): When you press the button, the machine uses the name you gave it to create a greeting message like « Hello, [Friend’s Name]! »
-
Greeting (Return Value): After pressing the button, the machine shows you the greeting message it created.
In JavaScript code, it might look like this:
// This is the "Greet" machine (function)
function greetFriend(name) {
let greeting = "Hello, " + name + "!";
return greeting;
}
// You press the button (call the function) and provide your friend's name
let friendName = "Alice";
let greetingMessage = greetFriend(friendName); // The machine gives you a greeting
console.log(greetingMessage); // You see the greeting: "Hello, Alice!"
Let’s use this function in a Custom Code :
exports.main = async (event, callback) => {
const email = event.inputFields['email'];
const firstName = event.inputFields.firstname;
function greetFriend(name){
const greeting = "Hello, "+name+" !"; // Hello, Antoine !
return greeting;
}
const functionResult = greetFriend(firstName);
console.log(functionResult);
callback({
outputFields: {
email: email
}
});
}
Create and use a JavaScript function with ChatGPT
It can be faster and easier to create a fuction with chatGPT here’s how I created a function to test if a car is a preimum one or not.
The function :
function isPremiumCar(carBrand) {
const premiumCarBrands = [
'Audi', 'BMW', 'Bentley', 'Jaguar', 'Land Rover', 'Lexus', 'Maserati', 'Mercedes', 'Porsche', 'Rolls-Royce', 'Tesla', // Add more premium brands here
];
return premiumCarBrands.includes(carBrand);
}
The complete Operations Hub custom code :
exports.main = async (event, callback) => {
const carManufacturer = event.inputFields.carmanufacturer;
function isPremiumCar(carBrand) {
const premiumCarBrands = [
'Audi', 'BMW', 'Bentley', 'Jaguar', 'Land Rover', 'Lexus', 'Maserati', 'Mercedes', 'Porsche', 'Rolls-Royce', 'Tesla', // Add more premium brands here
];
return premiumCarBrands.includes(carBrand);
}
const isCarPremium = isPremiumCar(carManufacturer);
callback({
outputFields: {
isCarPremium
}
});
}
A chat GPT prompt you can use :
Context : serverless function developement
Help Needed : I want you to help me to write a logic with JavaScript
Constrains : the code should we written in this format :
exports.main = async (event, callback) => {
// code here
callback({
outputFields: {
}
});
}
The logic should be written inside the main function.
If the logic returns data the data should be returned by the callback function inside the outputFields object.
When you get external data input as parameters. The data comes from event.inputFields object. E.g : you get the contact email adress then the email variable should be set like :
const email = event.inputFields.email;
logic to implement :
Conclusion
In this article I gave you dome basic knowledge to get started with Custom Coded Action in HubSpot. However to use the Custom Coded Action at their full potential you need solid JavaScript fudenmental. If you are interested I definitely recomend this training :
What HubSpot gives us by default
On the platform in a Workflow, if you add a Custom Coded Action block, you have an interface where you can select the programming language you would like to use. A dropdown to manage / use your API secrets etc…
Underneath, you can write your code, but in that small editor there’s no autocompletion, you can’t format automatically your code. You can’t automatically write doc blocks for your functions…
That’s why I prefer to write my code locally in my IDE, that the way I produce better code, and I can also keep track of my changes with Git.
If you are interested to set up something similar, I will explain in this article how to set up a Custom Coded Action enviroment locally.
A JavaScript CLI to re-create the environment
The main idea is to have a local environment which provides the same functions and limitation as HubSpot. That’s why I created a CLI where I re-created the functions which are unique to the HubSpot Custom Coded Actions.
With this tool you can use your own IDE and feel like home.
prerequisites
- JavaScript / Node.js / Git knowledge
- Git
- Node.js installed on your machine
Install the CLI
To install the CLI clone the following repository :
git clone https://github.com/Antoinebr/HubSpot-Operations-Hub-Custom-Coded-Action.git
Then install all the dependencies
cd HubSpot-Operations-Hub-Custom-Coded-Action
npm install
Start a new project
To start a new project type :
npm run init my-new-custom-coded-action
This will copy the folder __TEMPLATE__
which is a scaffolding of a Custom Coded Action
Understand the unique functions and variables
In this video, I will explain some specific functions and variable used in the Custom Coded Actions. Nothing complicated, but still very important.
Manage the secrets in HubSpot
In a Custom Coded Action you can manage your secrets, which is ideal to store an API key for example. Indeed, you do not want to have your API keys displayed in your code, you will want to have them as environment variable.
To create a new secret :
To access you secret use :
process.env.mySuperAPIKey;
console.log(process.env.mySuperAPIKey);
Use the contact, company… properties in the Custom Coded Action
To access the properties, first go to the section Property to include in code
. In that section, you can use any property of the current object, and you can choose the variable name you want.
In that example, we can access the contact’s email by using :
event.inputFields.email
Get the id of the current object
If you need the id of the current object enrolled in the WorkFlow you can use :
event.object.objectId
Run your code locally
Manage / access the secrets locally
To use a secret locally, add it to your .env
file at the root the cli project.
Set your secret in the .env
like so
privateAppToken = "vcf-no9-ggt09tnz-a522-4002-b012-jngpmsecx"
dropContactAPI= "qdkqsnksqnknvvvppmqmqsdds"
To access the secret in your code, in the cca.js
where you write your code use :
process.env.privateAppToken
Watch the video to learn more :
Manage / access the objects’ properties locally
To set a property to include in code locally, add to the ./your-cca/event.js
file the keys you need :
exports.events = {
object: {
objectId: 3401
},
inputFields: {
domainName : "google.com",
email : "hello@gmail.com"
}
}
To access it in your code
const domainName = event.inputFields.domainName;
const email = event.inputFields.email;
console.log('--> ', domainName, '-----> ', email)
Manage / access the current object’s id
If your code, uses the object’s ID of the current object in the workflow you can use.
event.object.objectId; // this returns the id it the current object
To set it up locally in the « event.js« ` update the value :
exports.events = {
object: {
objectId: 3401
},
inputFields: {
domainName : "google.com",
email : "hello@gmail.com"
}
}
Test the code
To test the code type in your terminal :
npm run cca ./your-cca/cca.js
This runs your code and display what will be returned by the Custom Coded Action from the output function.
As you can see :
This very basic Custom Coded Action :
exports.main = async (event, callback) => {
const domainName = event.inputFields.domainName;
const email = event.inputFields.email;
callback({
outputFields: {
domainName,
email
}
});
}
Will display this in your terminal :
Deploy it
To deploy it nothing easier, copy the cca.js and paste in HubSpot.
Having unique data in a CRM system is important because it can help to avoid duplicate contacts, which can be annoying for customers and can also lead to lost opportunities. For example, if a customer is contacted by multiple salespeople, they may become frustrated and less likely to do business with you. Additionally, duplicate contacts can make it difficult to track customer interactions and to identify opportunities to upsell or cross-sell products or services.
What is needed to deduplicate ?
To deduplicate you need to have a unique key.
A unique key is a column or a combination of columns that uniquely identifies a record in a database. Unique keys are used to deduplicate data by identifying and removing duplicate records. This can improve the accuracy and reliability of data.
As an example when you register on an app like Instagram or Twitter they ask for your phone number. They use the phone number as a unique key if they need to deduplicate.
Indeed, it’s quite easy to have multiple email addresses, you can also use another name and nickname, but few people have more than one phone number. So for an APP like Instagram a phone number is a pretty good unique key.
In the example below we have two records, with somewhat similar names, they maybe are duplicates. But what unique key could we use here ? As you can see the only common key value pair in the two records is carPlate so we can use this key to deduplicate.
How to look for duplicates when a new data is inserted in HubSpot ?
To avoid duplicates, each time we get a new record in our CRM we can ask HubSpot to give us the list of all the records with a matching key value pair.
In our example, each time we get a new contact, we can ask HubSpot the list of contacts with the property carPlate = RM-123-IE
If he gets more than one record back, it means we have a duplicate.
In HubSpot you can use a workflow to run a logic each time something happens in our CRM. In that example each time we get a new contact with the carPlate value set.
The request looks like this :
As you can see on this schema, the request will return those two records :
Once we identified two records, we can compare them and decide the one we keep.
Deduplication works if we found 2 records, if we find more than 2 the logic is a bit more complicated as we have to perform two merges. It’s what we call an ambiguous merge. In this article, we will put this concept aside.
Merging criterias
Now that we have our duplicated records, we have to merge them. But before, we have to choose the record we keep and the one we remove.
That logic is up to you, but there are common logics.
Keep the record created first
With that criteria, the oldest recorded will remain, and the new one will be merged. It’s the most common criteria to merge.
Keep the record created last
As you can expect with that criteria, the latest record will remain and the oldest one will be merged into the newest.
Keep the record most recently updated
Keep the record which had the most recent update
Keep the record most recent engagement
Keep the record with the most recent meeting, calls, tasks…
Keep the record with the oldest engagement
Keep the record with the oldest meeting, calls, tasks…
How to set up a deduplication workflow in HubSpot ?
Create a workflow based on the type of objects you want to dedupe.
In this example, I create a contact based Workflow.
Add a trigger
A HubSpot workflow trigger is an event that causes a workflow to start. For example, you could create a workflow that is triggered when a contact fills out a form or in our case when criteria is met.
When the contact has a carPlate set, the workflow will start and the contact will be enrolled in the workflow.
Add a Custom Coded Action block
A custom coded action block in an HubSpot Workflow is a block of JavaScript or Python code that you can add to a workflow to perform custom actions. It’s a super block that you can use to create your own logic inside a workflow.
In our case we are going to use a Custom Coded Action block to add a logic to deduplicate our contacts based an the carPlate value.
Add your private app token
In the secret section you can save your API keys, pass phrases… that you don’t want to display in your custom code.
To deduplicate we need to call the HubSpot API so you need to have a private app token with the corresponding scope.
The scopes for our custom coded actions should be at least : read companies, write companies, read contacts, write contacts
As name you can use the name you want, in my setup I use the name privateAppToken
Paste the Custom Coded Action
First remove all the existing code then if you want to deduplicate contacts you have to use this code , if you want to deduplicate companies use that one.
Edit the Options
At the top of the code you can set the options you want.
/*
*
* Edit your Secret Name here
*/
const SECRET_NAME = "privateAppToken"
/*
*
* Choose the dedupe key
*/
const DEDUPE_KEY = "carplate";
/*
* Choose the merging criteria you want in that list
*
* Possible values :
*
* most-recent-engagement
* oldest-engagement
* created-first
* created-last
* most-recently-updated
*/
const MERGE_CRITERIA_KEEP_THE_ONE = "created-first";
/*
*
* If the DRY_RUN is true, no merge will be performed
*/
const DRY_RUN = false;
SECRET_NAME :
If your secret name is not privateAppToken
edit the line :
const SECRET_NAME = "privateAppToken"
With your secret name like :
const SECRET_NAME = "myOwnPrivateAppName"
DEDUPE_KEY :
In my case If have the dedupe key carPlate
but yours will be different so edit this line :
const DEDUPE_KEY = "carplate";
with your own key like :
const DEDUPE_KEY = "yourOwnKey";
To find the key you have to use you can follow this video :
MERGE_CRITERIA_KEEP_THE_ONE :
Set the merging criteria you want to use between those possibilities :
- most-recent-engagement
- oldest-engagement
- created-first
- created-last
- most-recently-updated
DRY_RUN :
If const DRY_RUN = false;
then nothing will be merged, this option is interesting to test the logic.
Possible values :
const DRY_RUN = false;
const DRY_RUN = true;
Data outputs
In a Custom Coded Action, you can specify a return. In other words, what the Custom Coded Action will throw back to the Workflow. This Custom Coded Action returns couple of things
As an example :
To set the output underneath the code there’s a « Data outputs » section where you have to put the following :
It should look like this :
Turn the workflow on
TO turn the workflow on, you can follow this video :
NB : You can’t use the option Yes, enroll existing companies which meet the trigger criteria as of now
as it will trigger API rate limit error. ( I’m working on it to find a fix)
Télécharger node depuis les sources ( apt-get…)
Franchement, c’est assez pénible et c’est prompt à encore plus d’erreur. Car si vous êtes ici, c’est que quelque chose ne se passe pas comme prévu.
Bref, je vous déconseille cette solution.
Utiliser le package « n »
Le plus simple et le plus efficace, parce que ce package va permettre d’activer une version de node en particulier.
Pour l’installer rien de plus simple :
npm install -g n # Install n globally
Puis télécharger la version que l’on souhaite :
n <version> # Install node <version>
Pour activer la version de node que l’on souhaite rien de plus simple, il suffit de juste taper :
n
Ensuite, il faut sélectionner la version que l’on souhaite.
En cas de problèmes :
Vérifiez quelle version de Node est active sur votre machine avec :
which node
Cela devrait retourner :
/usr/local/bin/node
Homebrew
Si vous avez installé Node avec Homebrew ce n’est pas impossible que l’installation de cette nouvelle version avec le package n
n’active pas la version voulue.
Désinstaller node depuis Homebrew
brew uninstal node
Maintenant la seule version de Node activée devrait être celle qui vient de n
Et voilà, c’est terminé !
Enjoy ! 🙂
Le setup est extrêmement simple, il vous faudra l’accès à votre serveur Linux Debian ou Ubuntu. Il faudra aussi installer Apache2 si ce n’est pas déjà fait.
Déployez votre code sur le serveur
Je ne vais pas détailler ici la méthode pour téléverser votre code sur votre propre serveur, personnellement ma préférence va pour le déploiement via un repo Github, sur le serveur, j’effectue un git clone ou bien des git pull pour récupérer la dernière version du code.
Démarrez votre application
Ici vous avez le choix, pour faire au plus simple, personnellement, j’utilise le paquet Forever
forever start app.js
Créez un fichier de conf pour Apache
Comme nous volons déployer une application Node JS, l’application va être exécutée de manière locale sur le serveur, c’est-à-dire que l’application sera disponible sur localhost sur le serveur.
L’idée va t’être de demander à Apache de passer le trafic extérieur vers cette application locale. Pour cela, nous allons utiliser la fonction proxyPass d’ Apache.
Afin d’utiliser cette fonction, il faut bien évidemment qu’elle soit disponible dans votre configuration d’Apache2, pour ce faire, activez le mod suivant :
a2enmod proxy proxy_http
Une fois que ce mode est activé, créer un fichier de configuration.
Allez dans le dossier :
/etc/apache2/sites-available
Puis créez le fichier .conf suivant ce style de nomenclature
nomdusite.antoibrossault.com.conf
Le fichier se construit de manière suivante
<VirtualHost *:80>
ServerAdmin contact@masterOfTheUniverse.com
ServerName my-app.antoinebrossault.com
ProxyRequests off
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
<Location />
ProxyPass http://localhost:8080/
ProxyPassReverse http://localhost:8080/
</Location>
RewriteEngine on
RewriteCond %{SERVER_NAME} =my-app.antoinebrossault.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
Vous noterez que dans cet exemple notre application tourne un sur le port 8080.
Si votre application tourne sur un port différent bien évidemment, veuillez mettre à jour ces lignes.
Activez le site dans Apache2
a2ensite nomdusite.antoibrossault.com.conf
Lancez cert-bot de let’s encrypt pour obtenir un certicat SSL valide
Je ne vais pas détailler ici l’installation de cert-bot, mais c’est un outil extrêmement pratique pour récupérer, renouveler automatiquement les certificats de let’s encrypt.
certbot --apache
Vous devriez maintenant avoir votre application qui tourne avec un certificat SSL valide.
Conclusion
Cette approche n’est pas recommandée avec toutes sortes d’application, mais si vous êtes sur une application de test, expérimentale, c’est globalement une solution rapide et intéressante à avoir dans sa caisse à outils.
Dans un projet vue.js commencez par installer vueX
Installez vueX avec npm
npm install vuex --save
Créez un store
Créez un store dans votre application. J’ai décidé de créer un dossier store
à la racine de mon projet.
Dans le fichier ./store/store.js
// ./store/store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// Accesible with $store.state.count
const state = {
count: 3
};
export default new Vuex.Store({
state
});
Demandez à vue.js d’utiliser le store
Dans main.js j’importe mon store à de manière globale puis je demande à vue.js de l’utiliser.
import Vue from 'vue'
import App from './App'
import store from './store/store' // j'importe mon store
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store, // je demande de l'utiliser.
template: '<App/>',
components: { App }
})
Les Getters
Nous pouvons créer des méthodes pour accéder à notre store. Pour ce faire nous créons nos méthodes :
const getters = {
evenOrOdd : state => state.count % 2 === 0 ? 'even' : 'odd',
}
Maintenant nous avons une méthode pour interagir avec notre state. Mais comment y accéder depuis mon component ?
Il faut importer mapGetters
dans notre component
Créer une méthode computed
NB Pour utiliser d’autres computed
méthodes il faut utilise le spread operator.
computed :{
...mapGetters([
'evenOrOdd'
]),
hello: {
get(){ return 42 },
}
}
Mon composant final avec les getters
<template>
<div>
Counter : {{$store.state.count}}, counter is {{ evenOrOdd }}
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import post from './post.vue';
export default {
name: 'HelloWorld',
components : {post},
data(){
return{
}
},
computed :{
...mapGetters([
'evenOrOdd'
]),
hello: {
get(){ return 42 },
}
}
}
</script>
Les setters
// ./store/store.js
const state = {
count: 3
};
const mutations = {
increment (state){
state.count++
},
decrement (state){
state.count--;
}
}
const actions = {
increment: (context) => context.commit('increment'),
decrement: (context) => context.commit('decrement'),
incrementIfOdd: (commit, state) => {
if( (state.count + 1 ) % 2 === 0 ) commit('increment');
},
incrementAsync: ({commit}) => {
return new Promise( (resolve, reject) => {
setTimeout( () => {
commit('increment');
resolve();
}, 1000);
})
}
}
export default new Vuex.Store({
state,
getters,
mutations,
actions
});
Utilisez le store dans les composants
<template>
<div class="hello">
Counter : {{$store.state.count}}, counter is {{ evenOrOdd }}
<button @click="increment"> + </button>
<button @click="decrement"> - </button>
<button @click="incrementIfOdd"> + </button>
<button @click="incrementAsync"> async </button>
<post></post>
<p> {{hello}} </p>
</div>
</template>
<script>
import { mapGetters, mapActions} from 'vuex'
import post from './post.vue';
export default {
name: 'HelloWorld',
components : {post},
data(){
return{
}
},
computed :{
...mapGetters([
'evenOrOdd'
]),
hello: {
get(){ return 42 },
}
},
methods : mapActions([
'increment',
'decrement',
'incrementIfOdd',
'incrementAsync'
]),
}
</script>
Installer les containers
Le plus simple est d’utiliser un fichier docker-compose.yml
Créez le fichier docker-compose.yml suivant à la racine de votre projet.
php_apache:
image: webdevops/php-apache:7.1
links:
- db:mysql
ports:
- 8989:80
volumes:
- ./:/app/
- ./php.ini/:/opt/docker/etc/php/php.ini
db:
image: mariadb
ports:
- 3306:3306
volumes:
- "./data/:/var/lib/mysql"
environment:
MYSQL_ROOT_PASSWORD: examplepass
phpmyadmin:
image: corbinu/docker-phpmyadmin
links:
- db:mysql
ports:
- 8181:80
environment:
MYSQL_USERNAME: root
MYSQL_ROOT_PASSWORD: examplepass
Démarrer les containers
Dans votre projet tapez docker-compose up -d
à l’inverse docker-compose down
Pour vérifier les containers qui tournent actuellement docker ps
Utiliser les containers
Allez sur http://localhost:8989
pour accéder à votre projet.
configurer la bdd
Pour administrer la db depuis un db manager ( sequel pro.. )
host : 127.0.0.1
port : 3306
MYSQL_USERNAME: root
MYSQL_ROOT_PASSWORD: examplepass
NB : Pour connecter votre app à la db, utilisez : host : mysql
Tester la connection
Ici un simple test avec node.js
npm install mysql
var mysql = require('mysql');
var connection = mysql.createConnection({
host : '127.0.0.1',
user : 'root',
password : 'examplepass',
database : 'information_schema'
});
connection.connect();
connection.query('SELECT * FROM ALL_PLUGINS', function (error, results, fields) {
if (error) throw error;
console.log('The solution is: ', results[0] );
});
connection.end();
Configurer PHP
Si cette configuration est destinée au dev, vous voulez surement afficher les erreurs PHP.
Pour ce faire, créez un fichier php.ini
à la racine de votre projet.
Dans ce fichier ajoutez :
## PHP CONFIG
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
Un point important à sous-lignée sur Angular c’est sur l’affichage des images. En effet si nous avions le chemin vers une image dans notre array de donnée nous serions tentés d’essayer de l’afficher comme ceci :
<img src="{{product.images[0].full}}"/>
Mais cela ne fonctionnera pas :/ ! Pourquoi me direz-vous ? En fait le navigateur va tout de suite essayer d’afficher l’image sans attendre qu’angular interprète l’expression et la remplace par le bon chemin.
Les petits gars de chez Google ont pensé à tout avec une directive prévue pour l’occasion, la directive ng-src pour afficher notre image nous devons donc faire.
<img ng-src="{{product.images[0].full}}"/>
Faites bien attention à utiliser des accolades pour qu’angular interprète bien la propriété
ng-src avec une boucle
Reprendre un petit exemple concret ne fait pas de mal alors nous allons voir comment faire une boucle pour afficher des images issues d’un sous array de notre controller.
(function() {
var app = angular.module('gemStore', []);
app.controller('StoreController', function() {
this.products = gems;
});
var gems = [{
name: 'Azurite',
description: "Some gems have hidden qualities beyond their luster, beyond their shine... Azurite is one of those gems.",
shine: 8,
price: 110.50,
rarity: 7,
color: '#CCC',
faces: 14,
images: [
"images/gem-02.gif",
"images/gem-05.gif",
"images/gem-09.gif"
]
},{
name: 'Zircon',
description: "Zircon is our most coveted and sought after gem. You will pay much to be the proud owner of this gorgeous and high shine gem.",
shine: 70,
price: 1100,
rarity: 2,
color: '#000',
faces: 6,
images: [
"images/gem-06.gif",
"images/gem-07.gif",
"images/gem-10.gif"
]
}];
})();
<!DOCTYPE html>
<html ng-app="gemStore">
<head>
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
<script type="text/JavaScript" src="angular.min.js"></script>
<script type="text/JavaScript" src="app.js"></script>
</head>
<body class="list-group" ng-controller="StoreController as store">
<!-- Product Container -->
<div class="list-group-item" ng-repeat="product in store.products">
<h3>{{product.name}} <em class="pull-right">{{product.price | currency}}</em></h3>
<!-- Image Gallery -->
<div class="gallery">
<div class="img-wrap">
<img ng-src="{{product.images[0]}}" />
</div>
<ul class="img-thumbnails clearfix">
<!-- ICI NOTRE BOUCLE -->
<li class="small-image pull-left thumbnail" ng-repeat="image in product.images">
<img ng-src="{{image}}" />
</li>
<!-- ICI NOTRE BOUCLE -->
</ul>
</div>
</div>
</body>
</html>
ng-click
Une autre directive tout aussi importante qu’essentielle, c’est ng-click qui permet assigner une valeur à une expression qu’Angular pourra interpréter. Vous comprendrez mieux ce concept avec l’exemple ci-dessous.
Imaginons que dans notre application de magasin en ligne nous voulions afficher des informations dans des tabs. Le principe d’une tab est que lorsque l’on clique sur le titre JavaScript cherche le contenu correspondant et l’affiche et cache les autres.
<section>
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>
{{tab}}
</section>
Que ce passe-t-il dans ce code ? En fait c’est très simple lorsque l’on clique sur Description / Specs / Reviews on assigne à l’expression tab une valeur, respectivement 1,2,3.
Vous remarquerez que dans notre exemple on affiche le contenu de l’expression {{tab}}, et quand un clique sur un élément avec la directive ng-click la valeur de tab change !
2-way- Data Binding
Mais c’est vraiment étrange que cette expression ce mette à jour, non ? C’est en fait un principe important sur Angular.js le 2-way- Data Binding. En fait à chaque fois que la propriété change Angular re-interprète l’expression.
conditionner dans une directive
Dans notre exemple précédent, vous avez dû remarquer que pour le moment nous n’affichions que l’expression après le click sur une tab. Mais maintenant nous voulons rendre la chose un peu plus concrète en affichant réellement les tables. Il faut donc que nous puissant dire à Angular :
« Quand je click sur une tab et que tab change de valeur, affiche la tab correspondante ».
Afficher / Cacher… Cela vous dit quelque chose ? Oui ! Il faut utiliser les directives ng-show !
ng-show permet de vérifier une condition il suffit de lui mettre cette condition en paramètre
<section>
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>
<div class="panel" ng-show="tab === 1">
<h1>1 </h1>
<p> {{product.description}} </p>
</div>
<div class="panel" ng-show="tab === 2">
<h1>2 </h1>
<p> {{product.description}} </p>
</div>
<div class="panel" ng-show="tab === 3">
<h1>3 </h1>
<p> {{product.description}} </p>
</div>
</section>
Ce qui nous donne, si tab à la valeur 2 alors affiche le contenu de cet élément html. Voilà ce que cela donne dans notre cas concret de pannel.
ng-init mettre en place une valeur par défaut à une expression
Dans notre exemple précédant, nous aimerions peut-être pouvoir afficher une tab au chargement de la page. Sans avoir à cliquer dessus. En suivant notre logique, il faudrait donc pouvoir assigner une valeur par défaut à tab… Angular propose une directive permettant de réaliser cela, c’est la directive ng-init
<section ng-init="tab = 1">
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>
...
</section>
Dans notre exemple ng-init nous permet d’assigner tab à une valeur de 1 dès le chargement de notre page.
ng-class ajouter / retirer une class en fonction d’une expression
Si vous avez l’habitude de faire de l’intégration , vous savez que nous avons souvent besoin d’ajouter des class en fonction de certaines conditions. C’est notamment le cas dans les pannels ou nous devons ajouter une class .active pour signifier le pannel courant. Angular nous permet de faire ce genre d’opération simplement avec ng-class.
Voilà comment elle fonctionne :
<li ng-class="{active:tab === 1}"></div>
En premier nous spécifions la class ici (active) en second l’expression à confronter avec notre condition.
<ul class="nav nav-pills">
<li ng-class="{active:tab === 1}">
<a href ng-click="tab = 1"> Description</a>
</li>
<li ng-class="{active:tab === 2}">
<a href ng-click="tab = 2"> Specs</a>
</li>
<li ng-class="{active:tab === 3}">
<a href ng-click="tab = 3"> Reviews</a>
</li>
</ul>
Pas de logique dans les vues…
Si vous regardez les exemple précédents, on commence à voir apparaitre de la logique dans nos vues (là où sont affichées les infos pour l’utilisateur). Et comme vous le savez, Angular.js est basé sur le modèle MVC (modèle / vue / controller).
Notre logique doit donc aller dans un controller pour éviter une bouillie de code incompréhensible.
Création d’un nouveau controller
La première étape est donc de créer une directive ng-controller sur notre élément :
<section ng-controller="PannelController as pannel">
<ul class="nav nav-pills">
<li ng-class="{active:pannel.isSelected(1)}">
<a href ng-click="pannel.selectTab(1)"> Description</a>
</li>
<li ng-class="{active:pannel.isSelected(2)}">
<a href ng-click="pannel.selectTab(2)"> Specs</a>
</li>
<li ng-class="{active:pannel.isSelected(3)}">
<a href ng-click="pannel.selectTab(3)"> Reviews</a>
</li>
</ul>
<div class="panel" ng-show="pannel.isSelected(1)">
<h1>1 </h1>
<p> {{product.description}} </p>
</div>
<div class="panel" ng-show="pannel.isSelected(2)">
<h1>2 </h1>
<p> {{product.description}} </p>
</div>
<div class="panel" ng-show="pannel.isSelected(3)">
<h1>3 </h1>
<p> {{product.description}} </p>
</div>
</section>
la deuxième étape est de créer notre controller pour sa partie JavaScript.
app.controller("PannelController",function(){
this.tab = 1;
this.selectTab = function(setTab){
this.tab = setTab;
};
this.isSelected = function(checkTab){
if(this.tab === checkTab){
return true;
}
})
});
Explication, pour mettre notre class « .active » sur le bon élément on utilise la directive ng-class qui affichera la classe donnée en function d’une condition. Pour vérifier cette condition, on appelle dans notre controller la function (une function expression) de notre controller. Cette function vérifie si notre tab est actuellement sélectionné.
Par défaut c’est bien notre tab 1 qui est sélectionné, en étudiant notre controller on observe bien « this.tab = 1 » donc isSelected vérifie qu’elle est la valeur de tab.
selectTab est une function qui modifie la valeur de tab.
