{"id":280,"date":"2007-07-06T00:00:00","date_gmt":"2007-07-06T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/rss-newsfeed-workbench\/"},"modified":"2021-09-29T16:22:17","modified_gmt":"2021-09-29T16:22:17","slug":"rss-newsfeed-workbench","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/rss-newsfeed-workbench\/","title":{"rendered":"RSS Newsfeed Workbench"},"content":{"rendered":"<p>Have you ever fancied being able to put Simple-Talk&#8217;s content into a database? We haven&#8217;t either, but we&#8217;ll do it just for practice. The objective of this workbench is to show just how easy it is to implement an RSS-based datafeed in SQL Server 2005. The purpose is to try out downloading an XML file from the internet, checking it for validity, and if not, seeing if it is a redirect. We&#8217;ll show a cool SQL Server 2005 way of reading a text file into a <code>VARCHAR(MAX)<\/code> variable. We&#8217;ll do some shredding of an XML file too. By the time we&#8217;ve finished we hope we will have illustrated some useful techniques and given you an application that you can turn into a production-quality newsreader.<\/p>\n<p>It is very easy to read RSS feeds and OPML feeds into SQL Server 2005. This is because the files are XML and are easily eaten by the XML functions. RSS, which doesn&#8217;t actually stand for anything other than RDF Site Summary, is a simple way of publishing, or broadcasting, frequently updated Web content such as articles and blog entries. OPML, originally conceived for transferring outlines, is a format that has been used widely for distributing collections of RSS feeds. However, it has a large number of uses in transferring simple structured list-based information. As it is used for exchanging lists of RSS feeds, we&#8217;ll show how to read in an OPML list of feeds.<\/p>\n<p>Please remember before we start that this code is for illustration and is not &#8216;production quality&#8217;. You will need to sort out the security issues (see the process delegation workbench) and add logging and more error-checking.<\/p>\n<p>We won&#8217;t attempt to store all the information that RSS2 defines, simply because we can&#8217;t find any sites that use all this information. In fact, most feeds use only a small part of the information and few aggregators seem to display the extra information anyway. We&#8217;ll boil it down to two relational tables.<\/p>\n<p>Firstly, create a new database called RSSFeedWorkbench. Make sure you have the rights to execute <code>xp_cmdshell<\/code> too!<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">USE RSSFeedWorkbench  \r\nGO \r\nIF EXISTS ( SELECT  * \r\n            FROM    sysobjects --delete it if it exists \r\n            WHERE   id = OBJECT_ID('RSSitem') )  \r\n  DROP TABLE [RSSitem] \r\n\/* \r\nWe create two tables. RSSItem contains every news item that we collect.  \r\nIt is linked to an RSSFeed table which contains every feed that we want to  \r\nread from. We have not included every parameter of a feed item because \r\nthey are rarely used. *\/ \r\nCREATE TABLE dbo.RSSitem \r\n  ( \r\n    RSSitem_ID INT IDENTITY(1, 1) \r\n                   NOT NULL, \r\n    RSSFeed_ID INT NULL, \r\n    title VARCHAR(80) NOT NULL, \r\n    link VARCHAR(200) NULL, \r\n    [description] VARCHAR(MAX) NULL, \r\n    PubDate VARCHAR(40) NULL, \r\n    [GUID] VARCHAR(80) NULL, \r\n    insertiondate DATETIME NOT NULL \r\n                           CONSTRAINT dfRssItemDate DEFAULT ( GETDATE() ), \r\n    CONSTRAINT PK_RSSitem PRIMARY KEY CLUSTERED ( RSSitem_ID ASC ) \r\n  ) \r\nON \r\n  [PRIMARY]  \r\n\/****** Object:  Table  dbo . RSSFeed     Script Date: 07\/02\/2007 10:32:19 ******\/ \r\nIF EXISTS ( SELECT  * \r\n            FROM    sysobjects --delete it if it exists \r\n            WHERE   id = OBJECT_ID('RSSFeed') )  \r\n  DROP TABLE [RSSFeed] \r\n<\/pre>\n<p>The RSSFeed table contains every RSS feed that we want to read from. It is used to poll around the feeds. This routine checks to see whether the <code>TYPE<\/code> field is set to RSS (or RSS2 etc). If it isn&#8217;t then it isn&#8217;t read.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE TABLE dbo.RSSFeed \r\n  ( \r\n    RSSFeed_ID INT IDENTITY(1, 1) \r\n                   NOT NULL, \r\n    title VARCHAR(80) NOT NULL, \r\n    link VARCHAR(200) NOT NULL, \r\n    [description] VARCHAR(2000) NOT NULL \r\n                                DEFAULT '', \r\n    [language] VARCHAR(10) NOT NULL \r\n                           DEFAULT 'en-US', \r\n    Category VARCHAR(100) NOT NULL \r\n                          DEFAULT 'Generic', \r\n    docs VARCHAR(80) NULL, \r\n    Generator VARCHAR(80) NULL, \r\n    ManagingEditor VARCHAR(80) NULL, \r\n    Webmaster VARCHAR(80) NULL, \r\n    actualURL VARCHAR(200) NULL, \r\n    [type] VARCHAR(80) NOT NULL \r\n                       DEFAULT 'RSS',--is it RSS or ATOM? \r\n    insertiondate DATETIME NOT NULL \r\n                           DEFAULT ( GETDATE() ), \r\n    CONSTRAINT PK_RSSFeed PRIMARY KEY CLUSTERED ( RSSFeed_ID ASC ) \r\n  ) \r\nON \r\n  [PRIMARY]  \r\n\r\nGO \r\n\r\nALTER TABLE  dbo.RSSitem WITH CHECK \r\nADD \r\n  CONSTRAINT FK_RSSitem_RSSFeed FOREIGN KEY ( RSSFeed_ID ) \r\n      REFERENCES dbo.RSSFeed ( RSSFeed_ID ) \r\nGO \r\nALTER TABLE  dbo.RSSitem CHECK CONSTRAINT  FK_RSSitem_RSSFeed  \r\nGO \r\n\r\nCREATE NONCLUSTERED INDEX [idxRSSFeedTitle] ON [dbo].[RSSFeed] ( [title] ASC ) \r\n\r\nCREATE NONCLUSTERED INDEX [idxTitle] ON [dbo].[RSSitem] ( [title] ASC ) \r\nGO\r\n<\/pre>\n<p>Although there are more portable ways of getting the contents of the URL, we&#8217;ll use CURL.EXE, which is free, and very easy to install, and useful for a wide range of purposes. You&#8217;ll need to <a href=\"http:\/\/curl.haxx.se\/download.html\">download and install this<\/a> to get the Workbench to run. You&#8217;ll need to install the OpenSSL package too from <a href=\"http:\/\/www.slproweb.com\/products\/Win32OpenSSL.html\">here<\/a> .<\/p>\n<p>Some RSS feeds will try to stop non-browser software from reading an RSS feed. Curl has useful facilities that allow it to mimic a browser to the extent of allowing cookies and letting you specify the user agent.<\/p>\n<p>This is the stored procedure that grabs the contents of the feed, checks to see if is one we know about and, if not, saves its attributes. then it saves any items it has not already saved. As it saves the url of the feed, you can use it to add the feed to your repository, and then it will automatically refresh it (if you install the <code>spUpdateAllFeeds<\/code> stored procedure in the right place. This stored procedure is more complex than it might be because one can try to access a feed only to find it is a redirection to the actual feed. If this is the case then one has to read the file as an HTML file, get the anchor, and use the contents of the HREF as the source of the newsfeed, before trying again!<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">\r\nIF EXISTS ( SELECT  * \r\n            FROM    sysobjects \r\n            WHERE   id = OBJECT_ID('spUpdateRSSArchiveFrom') )  \r\n  DROP PROCEDURE [spUpdateRSSArchiveFrom] \r\nGO \r\nCREATE PROCEDURE [dbo].[spUpdateRSSArchiveFrom] \r\n  @url VARCHAR(200),--URL of the RSS datafeed to register and read from \r\n  @tempfile VARCHAR(80) = 'c:\\rssfeed.xml'--optional file to save as \r\nAS \/* \r\nexecute spUpdateRSSArchiveFrom  \r\n 'http:\/\/www.simple-talk.com\/community\/forums\/rss.aspx?ForumID=142&amp;Mode=0', \r\n 'C:\\forums.rss' \r\n\r\n*\/ \r\n  SET nocount ON \r\n  DECLARE @ErrorMessage VARCHAR(100) \r\n  DECLARE @command NVARCHAR(255) --the command string used for spExecuteSQL \r\n  DECLARE @ExitCode INT--the code returned by the xp_cmdShell procedure \r\n  DECLARE @ExitCodeASCII VARCHAR(10)--the ascii version of above code \r\n  DECLARE @Badfile VARCHAR(MAX)--we use this to pull in any HTML files to \r\n--examine if there was an error in reading in the XML file \r\n  DECLARE @Retries INT--the number of retries we allow \r\n  DECLARE @Ref VARCHAR(1000)--the reference supplied by the feed redirector \r\n  SELECT  @retries = 0--set up the retry count \r\n  CREATE TABLE #lines ( line VARCHAR(2000) )--table used to remember what is \r\n--passed back from the command line when executing xp_cmdShell \r\n\r\n  retry:--yes we use GOTOs. We sometimes do... \r\n  RAISERROR ( 'processing %s', 0, 1, @url )--a way of getting an immediate  \r\n--progress report \r\n\/* now we make up the command line for CURL.EXE. You may want to change the \r\nparameters here for various reasons such as a proxy. If an error happens \r\nthen whatever CURL prints out is returned to the procedure and printed out \r\nby the error processing *\/ \r\n  SELECT  @command = 'curl \"' + @url + '\" -o\"' + @tempfile \r\n         + '\" -A\"Mozilla\/4.0 (compatible; MSIE 6.0;' \r\n         +' Windows NT 5.0; .NET CLR 1.1.4322\"' \r\n\/*Firstly we read the RSSfeed into a file*\/ \r\n  INSERT  INTO #lines ( line ) \r\n          EXECUTE @exitcode= xp_cmdshell @command--and execute it \r\n  SELECT  @ErrorMessage = COALESCE(@errorMessage, '') + COALESCE(line, '') \r\n  FROM    #lines--put the entire result into one string \r\n  IF EXISTS ( SELECT  1 \r\n              FROM    #lines \r\n              WHERE   line LIKE '%is not recognized as an%' )  \r\n    BEGIN--the silly moo hasn't installed CURL! \r\n      RAISERROR ( 'Sorry. You must have CURL installed first!', 16, 1 ) \r\n      RETURN 1 \r\n    END \r\n  IF EXISTS ( SELECT  1--an error was returned from CURL \r\n              FROM    #lines \r\n              WHERE   line LIKE '%Could not resolve host%' ) \r\n    OR EXISTS ( SELECT  1 \r\n                FROM    #lines \r\n                WHERE   line LIKE 'curl: (%' )  \r\n    BEGIN \r\n      RAISERROR ( 'Sorry. Could not get RSS feed because %s', 16, 1, \r\n        @ErrorMessage ) \r\n      RETURN 1 \r\n    END \r\n  IF @Exitcode &lt;&gt; 0 --ah CURL command line returned an error code \r\n    BEGIN \r\n      SELECT  @ExitcodeASCII = CONVERT(VARCHAR(5), @Exitcode) \r\n      SELECT  @ErrorMessage = COALESCE(@errorMessage, '') + COALESCE(line, '') \r\n      FROM    #lines \r\n      RAISERROR ( 'Sorry. Errorcode %s. Curl reports %s', 16, 1, \r\n        @ExitcodeASCII, @ErrorMessage ) \r\n      RETURN 1 \r\n    END \r\n\r\n\r\n\/* ...then we read it in...*\/ \r\n  DECLARE @RSSfeed XML \r\n  DECLARE @Feed_ID INT \r\n\r\n  BEGIN TRY--we will catch errors at this point as they are usually XML \r\n    SELECT  @Command = 'SELECT  @RSSfeed = BulkColumn \r\nFROM    OPENROWSET(BULK ''' + @TempFile + ''', SINGLE_BLOB) AS x '  \r\n    EXEC sp_executesql @command, N'@RSSfeed xml output', @RSSfeed OUTPUT  \r\n  END TRY \r\n  BEGIN CATCH \r\n IF @@Error IN (9413,9422)--if it was an XML parsing error \r\n   BEGIN \r\n   RAISERROR ( '%s was not valid XML. Was it a redirect?', 0, 1, @url ) \r\n   SELECT  @Command = 'SELECT  @Badfile = BulkColumn \r\nFROM    OPENROWSET(BULK ''' + @TempFile + ''', SINGLE_BLOB) AS x '  \r\n   EXEC sp_executesql @command, --have a look at it \r\n   --read the file as a TEXT file rather than an XML \r\n     N'@BadFile varchar(MAX) output', @BadFile OUTPUT  \r\n   --we examine it to see if it is a redirect file \r\n   SELECT  @ref = SUBSTRING(@BadFille, \r\n            CHARINDEX('&lt;a href=\"', @BadFile + '&lt;a href=\"', 1) \r\n            + 9, 1000) \r\n   SELECT  @ref = LEFT(@Ref, CHARINDEX('\"', @ref + '\"') - 1) \r\n   IF LEN(@Ref) &lt; 10--check for obvious signs of problems \r\n     OR @ref NOT LIKE 'http:\/\/%'  \r\n     BEGIN \r\n     RAISERROR ( 'The url %s was not a valid redirect!', 16, 1, @URL ) \r\n     RETURN 1 \r\n     END \r\n        UPDATE RSSFeed SET type='BAD' WHERE ActualURL=@URL \r\n   SELECT  @URL = @ref, \r\n       @Retries = @Retries + 1 \r\n   IF @retries &lt; 3  \r\n     GOTO retry \r\n   END \r\n    RAISERROR ( 'The url %s was not a valid RSS feed!', 16, 1, @URL ) \r\n    RETURN 1 \r\n  END CATCH \r\n\/*Now we see if this is a new feed. If it is we add it to the feed  \r\ndatabase.  \r\n\r\n*\/ \r\n  DECLARE @RSSFeedAttributes TABLE \r\n    ( \r\n      title VARCHAR(80), \r\n      link VARCHAR(200), \r\n      [description] VARCHAR(2000), \r\n      [language] VARCHAR(10) DEFAULT 'en-US', \r\n      Category VARCHAR(100) DEFAULT 'Generic', \r\n      docs VARCHAR(80), \r\n      Generator VARCHAR(80), \r\n      ManagingEditor VARCHAR(80), \r\n      Webmaster VARCHAR(80) \r\n    )  \r\n\/* \r\n&lt;title&gt;My Title&lt;\/title&gt; \r\n&lt;link&gt;My Link&lt;\/link&gt; \r\n&lt;description&gt;My Description&lt;\/description&gt; \r\n&lt;language&gt;My Language e.g. en-us&lt;\/language&gt; \r\n&lt;category&gt;Newspapers&lt;\/category&gt;&lt;docs&gt;My Docs URL&lt;\/docs&gt; \r\n&lt;generator&gt;My RSS Generator&lt;\/generator&gt; \r\n&lt;managingEditor&gt;My.Editorial@Email.Address&lt;\/managingEditor&gt; \r\n&lt;webMaster&gt;My.Webmaster@Email.Address&lt;\/webMaster&gt; \r\n*\/ \r\n\r\n  INSERT  INTO @RSSFeedAttributes \r\n          ( \r\n            title, \r\n            link, \r\n            [description], \r\n            [language], \r\n            Category, \r\n            docs, \r\n            Generator, \r\n            ManagingEditor, \r\n            Webmaster \r\n             \r\n          ) \r\n   SELECT \r\n     x.feed.value('title[1]', 'varchar(80)') AS title, \r\n     x.feed.value('link[1]', 'varchar(200)') AS link, \r\n     x.feed.value('description[1]', 'varchar(2000)') AS [description], \r\n     COALESCE(x.feed.value('language[1]', 'Varchar(10)'), 'en-US')  \r\n                                                             AS [language], \r\n     COALESCE(x.feed.value('Category[1]', 'Varchar(100)'), \r\n          'Generic') AS [Category], \r\n     x.feed.value('docs[1]', 'Varchar(80)') AS [docs], \r\n     x.feed.value('generator[1]', 'Varchar(80)') AS [Generator], \r\n     x.feed.value('managingeditor[1]', 'Varchar(80)') AS [ManagingEditor], \r\n     x.feed.value('webmaster[1]', 'Varchar(80)') AS [Webmaster] \r\n   FROM    @RSSfeed.nodes('\/\/rss\/channel') AS x ( feed ) \r\n\r\n--insert the feed if it doesn't exist \r\n  INSERT  INTO RSSFeed \r\n          ( \r\n            title, \r\n            link, \r\n            [description], \r\n            [language], \r\n            Category, \r\n            docs, \r\n            Generator, \r\n            ManagingEditor, \r\n            Webmaster, \r\n            ActualURL \r\n             \r\n          ) \r\n          SELECT  f.title, \r\n                  f.link, \r\n                  f.[description], \r\n                  f.[language], \r\n                  f.Category, \r\n                  f.docs, \r\n                  f.Generator, \r\n                  f.ManagingEditor, \r\n                  f.Webmaster, \r\n                  @URL \r\n          FROM    @RSSFeedAttributes f \r\n                  LEFT OUTER JOIN RSSFeed  \r\n                          ON RSSfeed.title = f.title \r\n                          AND rssFeed.link = rssfeed.link \r\n          WHERE   rssFeed.rssFeed_ID IS NULL \r\n\r\n--..and get the ID of the feed \r\n  SELECT TOP 1 \r\n          @Feed_ID = rssFeed.rssFeed_ID \r\n  FROM    @RSSFeedAttributes f \r\n          INNER JOIN RSSFeed ON RSSfeed.title = f.title \r\n                                AND rssFeed.link = rssfeed.link \r\n--and add any RSSfeed item that doesn't exist \r\n  INSERT  INTO RSSitem \r\n      ( \r\n        RSSFeed_ID, \r\n        title, \r\n        link, \r\n        [description], \r\n        PubDate, \r\n        [GUID] \r\n      ) \r\n      SELECT  @feed_ID, \r\n              f.title, \r\n              f.link, \r\n              f.description, \r\n              COALESCE(f.pubdate, '-'), \r\n              COALESCE(f.[GUID], '-') \r\n      FROM    ( SELECT \r\n                    x.feed.value('title[1]', 'varchar(80)') AS title, \r\n                    x.feed.value('link[1]', 'varchar(200)') AS link, \r\n                    x.feed.value('description[1]', 'varchar(max)')  \r\n                                                     AS [description], \r\n                    x.feed.value('pubdate[1]', 'Varchar(40)') AS [pubdate], \r\n                    CONVERT(VARCHAR(80), x.feed.value('guid[1]', \r\n                                               'Varchar(2000)')) AS [GUID] \r\n                FROM    @RSSfeed.nodes('\/\/rss\/channel\/item') AS x ( feed ) \r\n              ) f \r\n      LEFT OUTER JOIN RSSitem ON COALESCE(f.title, '-') = RSSitem.title \r\n                         AND COALESCE(f.GUID, '-') = RSSitem.GUID \r\n                         AND COALESCE(f.PubDate, '-') = rssitem.pubdate \r\n      WHERE   rssitem.RSSitem_ID IS NULL \r\n\r\nGO \r\n\r\nIF EXISTS ( SELECT  * \r\n            FROM    sysobjects \r\n            WHERE   id = OBJECT_ID('spReadOPMLFile') )  \r\n  DROP PROCEDURE [spReadOPMLFile] \r\nGO \r\n\/* \r\nNow we provide a routine for reading in any ompl FILE. This is just a list \r\nso it is relatively easy to read, but note that the list may be  \r\nhierarchical. We are not too concerned about the OPML hierarchy so we \r\nwon't bother to record the hierarchy.\r\n*\/ \r\nCREATE PROCEDURE spReadOPMLFile @Filename VARCHAR(100) \r\n\/* \r\nspReadOPMLFile 'S:MyFavouriteRSSFeeds.opml' \r\n*\/ \r\nAS  \r\n  DECLARE @opmlfeed XML \r\n  DECLARE @command NVARCHAR(255) \r\n\r\n  SELECT  @Command = 'SELECT  @opmlfeed = BulkColumn \r\nFROM    OPENROWSET(BULK ''' + @filename + ''', SINGLE_BLOB) AS x '  \r\n  EXEC sp_executesql @command, N'@opmlfeed xml output', @opmlfeed OUTPUT  \r\n\r\n  INSERT  INTO RSSfeed \r\n      ( \r\n        title, \r\n        link, \r\n        [description], \r\n        [language], \r\n        Category, \r\n        actualURL, \r\n        [type] \r\n         \r\n      ) \r\n      SELECT  f.* \r\n      FROM     \r\n     ( SELECT   \r\n       x.opml.value('@title', 'nvarchar(80)') AS title, \r\n       x.opml.value('@xmlUrl', 'nvarchar(200)') AS link, \r\n       COALESCE(x.opml.value('@description', \r\n           'nvarchar(2000)'), '') AS [description], \r\n       COALESCE(x.opml.value('@language', \r\n           'nvarchar(2000)'), 'Een-US') AS [language], \r\n       'Generic' AS [category], \r\n       x.opml.value('@xmlUrl', 'nvarchar(200)') AS actualURL, \r\n       COALESCE(x.opml.value('@version', 'nvarchar(80)'), \r\n            'RSS') AS [type] \r\n     FROM    @opmlfeed.nodes('\/descendant::outline[@xmlUrl]') \r\n         AS x ( opml ) \r\n     ) f \r\n              LEFT OUTER JOIN rssfeed ON f.link = rssfeed.link \r\n      WHERE   rssfeed.rssFeed_ID IS NULL \r\n\r\nGO \r\nIF EXISTS ( SELECT  * \r\n            FROM    sysobjects \r\n            WHERE   id = OBJECT_ID('spUpdateAllFeeds') )  \r\n  DROP PROCEDURE [spUpdateAllFeeds] \r\nGO \r\n\r\n\/* this is the stored procedure that polls all your existing RSS feeds \r\n and reads in any new news entries*\/ \r\n\r\nCREATE PROCEDURE spUpdateAllFeeds \r\nAS  \r\n  DECLARE @command VARCHAR(MAX) \r\n\r\n  SELECT  @Command = COALESCE(@command, '') + ' \r\nexecute spUpdateRSSArchiveFrom ''' + actualURL + ''' \r\n' \r\n  FROM    rssFeed \r\n  WHERE   type LIKE 'RSS%' \r\n  EXECUTE ( @command )--execute them as a string \r\nGO\r\n<\/pre>\n<p>In the list of files to download will be some sample OMPL files to try out. Now when you&#8217;ve loaded in a few RSS feeds, all you have to do is to put this stored procedure on the scheduler and this will keep your News up-to-date. Instant Blogrolls! The first person to write an OMPL exporter for this will get a Simple-Talk goodie bag!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Robyn and Phil decide to build an RSS newsfeed in TSQL, using the power of SQL Server&#8217;s XML. &hellip;<\/p>\n","protected":false},"author":221812,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4150,4151,4252,4190,4460,4217,4799],"coauthors":[6813,6814],"class_list":["post-280","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-sql","tag-sql-server","tag-t-sql-programming","tag-tsql","tag-workbench","tag-xml","tag-xml-tsql-rss-opml-robyn-page"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/280","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221812"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=280"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/280\/revisions"}],"predecessor-version":[{"id":77220,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/280\/revisions\/77220"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=280"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=280"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=280"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=280"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}